Solving the bug in Zoho Writer API for styling

Solving the bug in Zoho Writer API for styling

So... the Zoho Writer APIs for programatically creating a document do not respect a template's style. The result is that any document you generate via API needs to be manually, paragraph by paragraph, reformatted.  That bug alone is sufficient to render Writer almost unworkable for any real-world usage.  (Who is responsible for product management and design quality? Yes, it is another example of developer-led programming short-cuts in Zoho!)

However, after a day of messing about, I found a workaround.  See the following markdown exploration of the issue and how to manually set the fonts, etc to match a templated document. The big problem with this is, of course, it requires a manual update should any templates change, and it needs to be aligned to every possible template.  Not ideal.

Zoho - can you please alter the way Zoho Writer handles importing basic formatting (eg. HTML headers, etc.) so that they respect the Writer template's existing styles.  Sure it can't be that difficult.   Also not, this happens if you insert RTF into a merge field... so it is not just the APIs.  The lack of basical style support means the entire templating system and generation of docs is broken from the ground up.  There is no way we can risk generating docs for automatic download or emailing with the product ion the current state. 

If I have missed something obvious in the documentation, please let me know. I would be very happy to wrotng about this problem.

For everyone else, enjoy the code.



  1. # How to Preserve Template Styles When Using the Zoho Writer Merge API

    ## The Problem

    When using the Zoho Writer Merge API to generate documents from templates, the merged content does not inherit the template's custom paragraph styles. Instead, standard HTML tags like `<h1>`, `<h2>`, `<h3>`, and `<p>` are rendered with default styling (Liberation Serif, browser-default sizes, black text, bold headings) — completely ignoring the font families, colours, and sizes defined in the template's stylesheet.

    This means if your template defines Heading 1 as *Heuristica 18pt dark blue*, your merged `<h1>` content will appear as *Liberation Serif 24pt bold black* instead.

    ## Root Cause

    The Zoho Writer Merge API accepts HTML in richtext merge fields. However, when it processes standard HTML heading tags (`<h1>` through `<h6>`), it:

    1. Maps them to Writer's internal heading classes (`heading1`, `heading2`, etc.)
    2. Applies **default inline styles** on the rendered spans — overriding the template's CSS-defined styles
    3. HTML heading tags also carry **implicit bold** (`font-weight: bold`) which may not match the template

    The template's stylesheet defines styles like:

    ```css
    #editorpane div.heading1 span.zw-portion {
        font-family: Heuristica, Georgia;
        font-size: 18pt;
        color: rgb(0, 32, 96);
    }
    ```

    But the merge engine ignores these and applies its own defaults inline.

    ## The Solution

    Instead of using semantic HTML heading tags, use `<p>` tags for **all** content and apply the template's exact styles as inline CSS. This prevents Writer from applying default heading styles, and gives you full control over the rendered appearance.

    ### Step 1: Extract Your Template's Styles

    Open your template in Zoho Writer, save the page as HTML (browser "Save As"), and inspect the stylesheet block. Look for the `#editorpane div.headingN span.zw-portion` rules:

    ```css
    /* Example from an IBRS template */
    #editorpane div.heading1 span.zw-portion {
        font-family: Heuristica, Georgia;
        font-size: 18pt;
        color: rgb(0, 32, 96);
    }
    #editorpane div.heading2 span.zw-portion {
        font-family: Heuristica, Georgia;
        font-size: 14pt;
        color: rgb(192, 0, 0);
    }
    #editorpane div.heading3 span.zw-portion {
        font-family: Heuristica, Georgia;
        font-size: 12pt;
        color: rgb(128, 128, 128);
    }
    #editorpane span.zw-portion {
        font-family: 'Lato 2';
        font-size: 12pt;
    }
    ```

    ### Step 2: Build HTML with Inline Styles

    Instead of:

    ```html
    <!-- DON'T DO THIS — styles will be overridden -->
    <h1>Cloud Infrastructure Advisory</h1>
    <h2>Executive Summary</h2>
    <p>This report examines cloud adoption trends.</p>
    <ul>
        <li>Finding one</li>
        <li>Finding two</li>
    </ul>
    ```

    Do this:

    ```html
    <!-- DO THIS — inline styles match the template exactly -->
    <p style="font-family: Heuristica, Georgia; font-size: 18pt; color: rgb(0,32,96);">Cloud Infrastructure Advisory</p>
    <p style="font-family: Heuristica, Georgia; font-size: 14pt; color: rgb(192,0,0);">Executive Summary</p>
    <p style="font-family: 'Lato 2', Lato, sans-serif; font-size: 12pt;">This report examines cloud adoption trends.</p>
    <ul>
        <li style="font-family: 'Lato 2', Lato, sans-serif; font-size: 12pt;">Finding one</li>
        <li style="font-family: 'Lato 2', Lato, sans-serif; font-size: 12pt;">Finding two</li>
    </ul>
    ```

    Key points:
    - Use `<p>` tags for **everything** including headings — never `<h1>` through `<h6>`
    - Apply the template's exact `font-family`, `font-size`, and `color` as inline styles
    - Do **not** add `font-weight: bold` unless the template explicitly uses it
    - List items (`<li>`) also need inline styles

    ### Step 3: Pass to the Merge API

    Use the v2 Merge and Store endpoint to create a persistent Writer-native document:

    ```python
    import json
    import requests

    ACCESS_TOKEN = "your_access_token"
    TEMPLATE_ID = "your_template_id"  # The merge template document ID

    # Your styled HTML content
    styled_html = (
        '<p style="font-family: Heuristica, Georgia; font-size: 18pt; '
        'color: rgb(0,32,96);">Cloud Infrastructure Advisory</p>'
        '<p style="font-family: Heuristica, Georgia; font-size: 14pt; '
        'color: rgb(192,0,0);">Executive Summary</p>'
        '<p style="font-family: \'Lato 2\', Lato, sans-serif; font-size: 12pt;">'
        'This report examines cloud adoption trends.</p>'
    )

    # Merge data must be wrapped in {"data": [...]}
    merge_data = json.dumps({
        "data": [{"content": styled_html}]
    })

    # Output settings — use "zdoc" for native Writer documents
    output_settings = json.dumps({
        "doc_name": "My Report",
        "format": "zdoc"
    })

    response = requests.post(
        f"{BASE_URL}/writer/api/v2/documents/{TEMPLATE_ID}/merge/store",
        headers={"Authorization": f"Zoho-oauthtoken {ACCESS_TOKEN}"},
        data={
            "merge_data": merge_data,
            "output_settings": output_settings,
        },
    )

    result = response.json()
    record = result["records"][0]
    print(f"Document URL: {record['document_url']}")
    print(f"Document ID:  {record['document_id']}")
    ```

    ## Additional Gotchas Discovered

    ### 1. merge_data format

    The merge data **must** be wrapped in a `{"data": [...]}` array, even for a single record:

    ```json
    // WRONG — will return "Merge data not found" (R6001)
    {"content": "<p>Hello</p>"}

    // CORRECT
    {"data": [{"content": "<p>Hello</p>"}]}
    ```

    ### 2. Output format for editable documents

    Use `"format": "zdoc"` to create native Zoho Writer documents. Using `"format": "docx"` creates a Word file in WorkDrive that doesn't behave as a native Writer document.

    ### 3. No newlines in HTML content

    Do not include `\n` newline characters between HTML elements in the merge data. Writer renders these as literal line breaks in the document. Concatenate your HTML elements directly:

    ```python
    # WRONG — \n appears as visible line breaks
    html = "<p>Paragraph one</p>\n<p>Paragraph two</p>"

    # CORRECT
    html = "<p>Paragraph one</p><p>Paragraph two</p>"
    ```

    ### 4. OAuth scopes required

    The merge and store endpoint requires these scopes:

    ```
    ZohoWriter.documentEditor.ALL
    ZohoWriter.merge.ALL
    ZohoPC.files.ALL
    WorkDrive.files.ALL
    WorkDrive.organization.ALL
    WorkDrive.workspace.ALL
    ```

    The first two alone are not sufficient — the WorkDrive and ZohoPC scopes are also required, even for basic template and folder operations.

    ### 5. Template field type must be "richtext"

    Your merge template must have a field defined as `richtext` type to accept HTML content. You can verify fields via:

    ```
    GET /writer/api/v1/documents/{template_id}/fields
    ```

    Which returns:

    ```json
    {
        "merge": [
            {
                "id": "content",
                "type": "richtext",
                "display_name": "content",
                "category": "created"
            }
        ]
    }
    ```

    ## Helper Function: Content Builder

    Here is a reusable Python function that converts a structured content specification into correctly styled HTML for any template:

    ```python
    from html import escape

    # Define styles per template — extracted from the template's stylesheet
    TEMPLATE_STYLES = {
        "my_template": {
            "heading1": "font-family: Heuristica, Georgia; font-size: 18pt; color: rgb(0,32,96);",
            "heading2": "font-family: Heuristica, Georgia; font-size: 14pt; color: rgb(192,0,0);",
            "heading3": "font-family: Heuristica, Georgia; font-size: 12pt; color: rgb(128,128,128);",
            "paragraph": "font-family: 'Lato 2', Lato, sans-serif; font-size: 12pt;",
            "list_item": "font-family: 'Lato 2', Lato, sans-serif; font-size: 12pt;",
        },
    }

    def build_html(content_blocks, template_name=None):
        """
        Convert structured content blocks into styled HTML.

        content_blocks: list of dicts with keys:
            - type: "heading1", "heading2", "heading3", "paragraph",
                    "bullet_list", "numbered_list"
            - text: str (for headings/paragraphs)
            - items: list[str] (for lists)
        template_name: key into TEMPLATE_STYLES dict
        """
        styles = TEMPLATE_STYLES.get(template_name, {}) if template_name else {}
        parts = []

        for block in content_blocks:
            block_type = block.get("type", "paragraph")
            text = block.get("text", "")
            items = block.get("items", [])

            if block_type in ("heading1", "heading2", "heading3", "paragraph"):
                style = styles.get(block_type, "")
                attr = f' style="{style}"' if style else ""
                parts.append(f"<p{attr}>{escape(text)}</p>")

            elif block_type == "bullet_list":
                style = styles.get("list_item", "")
                attr = f' style="{style}"' if style else ""
                li = "".join(f"<li{attr}>{escape(item)}</li>" for item in items)
                parts.append(f"<ul>{li}</ul>")

            elif block_type == "numbered_list":
                style = styles.get("list_item", "")
                attr = f' style="{style}"' if style else ""
                li = "".join(f"<li{attr}>{escape(item)}</li>" for item in items)
                parts.append(f"<ol>{li}</ol>")

        return "".join(parts)  # No \n between elements!


    # Usage
    content = [
        {"type": "heading1", "text": "Cloud Infrastructure Advisory"},
        {"type": "heading2", "text": "Executive Summary"},
        {"type": "paragraph", "text": "This report examines cloud adoption."},
        {"type": "bullet_list", "items": ["Finding one", "Finding two"]},
        {"type": "heading3", "text": "Recommendations"},
        {"type": "numbered_list", "items": ["Action one", "Action two"]},
    ]

    html = build_html(content, template_name="my_template")
    ```

    ## Summary

    | What | Don't | Do |
    |------|-------|----|
    | Heading tags | `<h1>`, `<h2>`, `<h3>` | `<p style="...">` |
    | Styling | Rely on template CSS inheritance | Inline the template's exact styles |
    | Bold | Let heading tags add implicit bold | Only add `font-weight` if the template uses it |
    | merge_data | `{"field": "value"}` | `{"data": [{"field": "value"}]}` |
    | Output format | `"docx"` | `"zdoc"` for native Writer |
    | HTML joins | `"\n".join(parts)` | `"".join(parts)` |

    This approach preserves the visual identity of your Zoho Writer templates when programmatically generating documents through the Merge API.

    ---

    *Tested with Zoho Writer API v1/v2, April 2026*




    • Sticky Posts

    • [Announcement] Insert image from URL changes in Zoho Writer

      Hi Zoho Writer users! We'd like to let you know that we've changed the behavior of the Insert image from URL option in Zoho Writer for security reasons. Earlier behavior Once you inserted an image URL in a Writer document, the image would be fetched from
    • Deprecation of certain URL patterns for published Zoho Writer documents

      Hi Zoho Writer users! We'd like to let you know that we have deprecated certain URL patterns for published and embedded documents in Zoho Writer due to security reasons. If the published or embedded documents are in any of these URL patterns, then their
    • Introducing plagiarism checker in Zoho Writer

      Zia, Zoho Writer's AI-driven writing assistant, now highlights plagiarized and duplicated content in addition to offering contextual grammar and writing suggestions to help you write clearly and concisely in English. Zoho Writer's plagiarism reports Here
    • [Important announcement] Impact of Google's new email guidelines for Zoho Writer automation users

      Hi users, Google has recently announced new guidelines for sending emails to Gmail and other Google-hosted domains. These guidelines will be effective starting Feb. 1, 2024, and can impact the delivery of emails sent from Zoho Writer. Your organization
    • Transitioning from MS Word to Writer: A complete walkthrough

      Hello everyone! We understand moving to a new word processing tool can be difficult, especially if it means switching from a legacy software like MS Word. That's why we've organized an exclusive webinar where we talk you through ways to make your switch from MS Word to Writer as easy as possible. In this webinar, you'll learn: - Why Writer is a simple yet powerful alternative to MS Word. - How to locate your favorite MS Word features and functions in Writer.  - How to migrate your Word documents

    Nederlandse Hulpbronnen


      • Recent Topics

      • Bank Feeds Missing some transactions and duplicating some

        Hello, I have been using Zoho or a number of years and have never had a real problem with bank feeds, however in the last month the feeds have not picked up all transactions, and sometimes a transaction is being duplicated. For example, the bank statement
      • Direct URL to "View Record"

        I would like to create a button (Deluge Script) to jump to the "view record" without passing by the record menu, but I can not aford to find the right url. I don't want the "view record" as a single page. I want the "view record" in the application like the picture below: Thanks in advance for your help
      • Introducing Built-in Telephony in Zoho Recruit

        We’re excited to introduce Built-in Telephony in Zoho Recruit, designed to make recruiter–candidate communication faster, simpler, and fully traceable. These capabilities help you reduce app switching, handle inbound calls efficiently, and keep every
      • Allowed IP Addresses: Manage where users access your organization's email from

        Securing organizational email takes more than strong passwords and multi-factor authentication. The location from which a user signs in is just as important as the credentials they use, especially when the account holds sensitive business information.
      • How to set up on-demand badge printing?

        Hello, on the introduction page (https://www.zoho.com/backstage/check-in-and-badging.html), it is stated that on-demand badge printing is possible, i.e. printing the corresponding badge after check in via scanning the attendee's QR code. How can I set
      • Enhance Barcode/QR Code scanner with bulk scanning or continuously scanning

        Dear Zoho Creator, As we all know, after each scan, the scanning frame closes. Imagine having 100 items; we would need to tap 100 times and wait roughly 1 second each time for the scanning frame to reopen on mobile web. It's not just about wasting time;
      • What’s New in Zoho Inventory — Latest Features, Integrations & Updates | December 2025

        Zoho Inventory has evolved significantly over the past months, bringing you smarter, faster, and more connected tools to streamline your operations. Whether you’re managing multichannel sales, complex fulfillment workflows, or fast-moving stock, our newest
      • Creating Email template that attaches file uploaded in specific field.

        If there's a way to do this using Zoho CRM's built-in features, then this has eluded me! I'm looking to create a workflow that automatically sends an email upon execution, and that email includes an attachment uploaded in a specific field. Email templates
      • Edit 'my' Notes only

        The permissions around Notes should be more granular, and allow to user to be able to edit the notes he created only. The edit Notes permission is useful as it allows the user to correct any mistakes or add information as needed. However, with this same
      • Restrict Ticket Movement to Specific Departments in Zoho Desk

        Hello Zoho Desk Team, Greetings, and hope you are doing well. We would like to submit a feature request regarding the ability to control ticket movement between departments in Zoho Desk. Current Limitation: At present, Zoho Desk allows agents to move
      • GEO and Zoho Desk

        Has anyone done anything with GEO (generative engine optimization) and zoho desk KB? Are there any plans from Zoho on adding the ability in inject GEO scripts in KB Articles?
      • Incoming Threads Report

        From data to decisions: A deep dive into ticketing system reports Customers raise questions and issues through multiple channels, such as email, chat, or tickets. To monitor the number of queries received on a specific day from each channel, leads can
      • Block opening tickets vía email DESK

        Hello, I want to block the functionality of opening tickets when someone send an email to our support email address. Actually everybody in the world can open a ticket in our systen just sending an email to our support email address I don´t want this feature!!!!
      • Autocomplete by email address

        Hi, Is it possible to add contacts via autocomplete using the email address? I have tried a number of contact option variations and cannot seem to get it to work, having this as an option would speed up composing an email greatly. thanks Ben
      • Switch between multiple LLMs instantly for tailored Zia experiences

        Availability Editions: Professional , Enterprise, Ultimate , CRMPlus , ZohoOne Release Plan: Available for all DCs Hello everyone. Earlier, the Multi-LLM feature supported only one LLM at a time for Zia Record Assistant bot restricting flexibility from
      • Subscriptions for service call

        So we install products and we want to offer a service contract for the customers yearly service calls to be billed monthly. So ideally at some point we want to email them a quote for their needs. WE will choice it our end based on the equipment. It would
      • PHPMailer "Connection refused (111)" Error Despite Outbound Ports Being Open

        Hello everyone, I'm trying to send emails via Zoho SMTP using PHPMailer on my shared hosting environment. I've tried both port 465 (ssl) and 587 (tls), and I even added SMTPOptions to bypass SSL verification just in case it was an SSL certificate issue.
      • Power up your Kiosk Studio with Real-Time Data Capture, Client Scripts & More!

        Hello Everyone, We’re thrilled to announce a powerful set of enhancements to Kiosk Studio in Zoho CRM. These new updates give you more flexibility, faster record handling, and real-time data capture, making your Kiosk flows smarter and more efficient
      • Cliq iOS can't see shared screen

        Hello, I had this morning a video call with a colleague. She is using Cliq Desktop MacOS and wanted to share her screen with me. I'm on iPad. I noticed, while she shared her screen, I could only see her video, but not the shared screen... Does Cliq iOS is able to display shared screen, or is it somewhere else to be found ? Regards
      • Rename system-defined labels in Zoho CRM

        Renaming system-defined labels is now available across all DCs. Hello everyone, Zoho CRM includes predefined system fields across modules to support essential CRM operations. Until now, the labels of these fields were fixed and could not be edited from
      • What happens to my current site SEO if i opt for zoho creator?

        I have an existing website and I need to use Zoho creators for the rapid creation of my webpage creation. Currently, my IT team is creating the web pages, but I am concerned about the SEO of my current website if I shift to zoho will i loose it all?
      • Filter Pivot Chart by Criteria

        Hey There Amazing Zoho Team, I wanted to see if there is a way to filter criteria a "Pivot Chart" report in the same way I can filter "List", "Calendar", & "Summary" reports. Example: I can filter a normal report like this: http://monosnap.com/image/y5q0XGzDSGZpsnOjuBYpdeUOFtSmFd I would like to do the same with a "Pivot Chart".  What I've Tried: Filters and User filters are not what I am looking for. That just filters the data. I want to be able to filter criteria the data by connected forms. Like
      • Big Time HELP

        I am old, disabled and need to speak to a person. I needed to use a service to copy my zoom contacts to. I think I signed up for a security service, which I do not need. I don't know enough to choose from your many lists or how to see what I have and
      • #4 Setting Up Your Client the Right Way

        Creating invoices without properly setting up the customer can quickly lead to several inconsistencies, such as duplicate records, missing billing details, and poor customer management. This becomes even more important for businesses that use a connected
      • Functionality based URL to find records due today or in the next week

        I need to construct a URL to filter a view to records where the evaluation is due soon (i.e. in the next 7 days). To me, that includes today, but Zoho has a different opinion. Zoho defines 39 as NEXT_7_DAYS but this gives unexpected results. https://creatorapp.zoho.eu/...#Report:My_Evaluations?Evaluation_due_by_op=39
      • Email Parameter in Create Lead API

        In the Create Lead API , the email parameter as mandatory. This is creating issues because many leads only have mobile numbers and no email address. This is especially a problem for businesses focusing on WhatsApp marketing in Zoho Marketing Automation,
      • Automatic Email Alerts for Errors in Zoho Creator Logs

        Hello, We would like to request a feature enhancement in Zoho Creator regarding error notifications. Currently, Zoho Creator allows users to view logs and errors for each application by navigating to Zoho Creator > Operations > Logs. However, there is
      • Filter our rejected quote items from the inventory quote template

        Hello, I am trying to have rejections at the line level on my quotes so I can track what items are often removed, I do not want to claim the whole quote as lost just the individual items for better data tracking. However I cannot figure out how to filter
      • Configuring ZMA Webhook for Zoho Flow : Missing Key Information Error

        I'm looking to send a webhook to Zoho Flow at the end of a Marketing Automation journey in order to perform more actions that I can't do with just a journey. I have the Webhook created in Flow and set up in ZMA, however when I test it I get the error
      • Improve Zoho Vault search

        Hello, we started using Zoho Vault and we really appreciate the app. At the moment, when searching passwords, the search query only targets the password name. I think having the query search among Name, User name, URL, Notes and Tag would be a better
      • Introducing the New Zoho Assist Quick Support Plugin

        We are thrilled to announce the new Zoho Assist Quick Support Plugin, the upgraded and enhanced version of the Zoho Assist Customer Plugin. This new plugin allows organizations and IT administrators to deploy it directly onto their customers’ devices,
      • NEED HELP

        I have below data po no invoice no Party inv date value status 1 aa1 ABC 01-May-26 100 Under Cr 2 aa2 XYZ 02-May-26 200 Over Due 3 aa3 ABC 02-May-26 300 Under Cr 4 aa4 XYZ 03-May-26 400 Under Cr I need result as below using formulas (not Pivot table)
      • Ask the Experts 29: Knowledge Base, Community, and AI for smarter user education

        Hi Everyone, Welcome to Ask the Experts (ATE) 29, a live panel discussion. After the engaging text-based discussions in ATE 27 on onboarding and managing agents and ATE 28 on handling customer support with AI, we’re bringing you the next round of ATE
      • Bigin, more powerful than ever on iOS 26, iPadOS 26, macOS Tahoe, and watchOS 26.

        Hot on the heels of Apple’s latest OS updates, we’ve rolled out several enhancements and features designed to help you get the most from your Apple devices. Enjoy a refined user experience with smoother navigation and a more content-focused Liquid Glass
      • Tables for Europe Datacenter customers?

        It's been over a year now for the launch of Zoho Tables - and still not available für EU DC customers. When will it be available?
      • Smart Feature Compatibility Indicators for CRM Field

        Zoho CRM offers a wide range of field types and advanced customization options. However, several field types have feature-specific limitations that are currently documented only in help articles. For example, while configuring a Rich Text field, admins
      • 📢 Coming Soon: Daily Rate Projects

        Hello everyone, We're excited to announce a long-awaited, new billing type for projects: Daily Rate! Until now, projects supported Fixed Cost and Hourly Rate billing. With this update, you can now create projects billed on a daily rate, making it easier
      • 📢 Coming Soon: Daily Rate Projects

        Hello everyone, We're excited to announce a long-awaited, new billing type for projects: Daily Rate! Until now, projects supported Fixed Cost and Hourly Rate billing. With this update, you can now create projects billed on a daily rate, making it easier
      • Cannot find IMAP/SMTP enable toggle in new Mail Admin UI - Mail Lite plan

        Hi Zoho team, I'm the org Super Admin for owlmind.dev (Mail Lite plan, 3 paid users): - champ@owlmind.dev (Super Admin) - isfand@owlmind.dev - team@owlmind.dev I need to enable IMAP and SMTP access for all 3 users (we're integrating with Smartlead for
      • Kaizen #242 - Enabling In-Context Order Creation from Deals Using SlyteUI

        Hello everyone! Welcome to another interesting Kaizen post. Today’s spotlight is on SlyteUI, the new UI builder designed to create powerful, intuitive user interfaces in minutes. Built for speed and simplicity, SlyteUI empowers teams to deliver high-impact
      • Next Page