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
    • Recent Topics

    • アナリティクスで商談中のパイプライン(ステージ)の件数比較

      アナリティクスで商談中のパイプライン(ステージ)の件数を前週と前々週で比較したい。前々週の件数が更新することで変動してしまう。対象方法をご教授ください。
    • How do I remove a data source from Zoho Analytics?

      I am unable to find a delte option on a datasource that i put in the system as an error. On teh web it refers to a setup icon but I do not see that on my interface?
    • Exclude Email or Domain From New Ticket Notification

      Hi, we utilize the new ticket notification feature in Zoho Desk. However, it would be great if there was a way to exclude certain email addresses or domains from receiving the automatic notification. This would be particularly helpful for automated alerts
    • Identify and clean hard bounce lists in Automation 2.0

      Hello. 1. I want to know how I can identify hard bounces in the lists I created to clean them before sending an email, given that the bounce rate has increased and it is necessary to clean the lists. 2. How can I exclude hard bounces and invalid emails
    • Trigger workflows from SLA escalations in Zoho Desk?

      Hey everyone, I’m currently working with SLA escalation rules in Zoho Desk and ran into a limitation that I’m hoping someone here has solved more elegantly. As far as I can tell, SLA escalations only support fairly limited actions (like changing the ticket
    • Delete a department or category

      How do I delete a Department? Also, how do I delete a Category? This is pretty basic stuff here and it's impossible to find.
    • Zoho Webinar - Sharing System Audio (NOT AVAILABLE)

      Hi, We are having a serious problem with Zoho Webinar. In the webinars we run, we very often share the audio from a video we are streaming directly from YouTube or other applications. Until recently we were using Zoom, but as we use other Zoho applications
    • Lost the ability to sort by ticket owner

      Hi all, in the last week or so, we have lost the ability to sort tickets by Ticket Owner. Unlike the other columns which we can hover over and click on to sort, Ticket Owner is no longer clickable. Is it just us, or are other customers seeing this too?
    • Cancellation Fees

      Hi, It really would be good if Billing could take subscription management further with cancellations & being able to apply or set a cancellation fee for a plan that is either fixed or prorated. It is not uncommon in subscriptions for cancellation fees
    • Custom Field for Subscription

      Hi, I can't find a way to add a custom field (to contain a license key generated from our software) against a subscription? Is the only place to add this information in the Invoice module (as custom field for invoice)? When a customer views his subscription
    • Zoho CRM Meetings Module Issues

      We have a use-case that is very common in today's world, but won't work in Zoho CRM. We have an SDR (Sales Development Rep) who makes many calls per day to Leads and Contacts, and schedules meetings for our primary Sales Reps. He does this by logging
    • Notes - Reaction Buttons

      Using the native notes option within CRM is fine, it works and the RTF features are great, however, would it be possible - if there isnt already something in place, where we can add a reactions button, similar to teams/whatsapp to show that its been read
    • How to get the campaingns key?

      Reading the documentations of the API, I see that is necessary have the campaign key, but I don't see how can I get it. For example to get the campaign details we need to do the request: https://campaigns.zoho.com/api/getcampaigndetails?authtoken=[API Authentication Token]&scope=CampaignsAPI&campaignkey=[campaignkey] I have the API Authentication Token but I don`t see how to generate the campaignkey
    • Unable to switch existing AWS RDS connection to DataBridge after moving RDS behind VPN

      Unable to switch existing AWS RDS connection to DataBridge after moving RDS behind VPN Hi everyone, I’m facing a problem with an existing Zoho Analytics setup and would like to know the best migration path. Originally, my Zoho Analytics connection to
    • [Bug] WebAuthn passkey registration blocked on rpIds with TLDs longer than 6 characters (.accountant, .technology, etc.) — isValidDomain regex too strict

      Hi, Filing on behalf of an enterprise customer where Zoho Vault is deployed across the company. The Chrome extension blocks WebAuthn passkey registration on legitimate sites whose Relying Party ID (rpId) has a TLD longer than 6 letters. This affects every
    • Native QuickBooks integration for Zoho CRM: Connecting sales and finance

      Greetings, I hope all of you are doing well. We're excited to announce Zoho CRM's integration with QuickBooks Web, which is designed to synchronize your CRM data with your QuickBooks accounting records and bridge the gap between sales and finance. This
    • Syncing zoho books into zoho crm

      I was wondering how I can use zoho books in crm as I have been using them separately and would like to sync the two. Is this possible and if so, how? Thanks
    • ZohoBooks_add_expense_attachment Fails

      I'm working MCP in Claude to automate bookkeeping. Claude cannot seem to attach and reciept to an expense. The 'add expense attachment' tool is added to the server and enabled in Claude. I asked Claude to give me the calls he performed and this is what
    • ZohoBooks_create_chart_of_account

      I'm setting up Claude to do my bookkeeping workflows using a Zoho MPC server I setup. He does not seem to be able to create a chart of account. The 'create chart of account' tool is added to the server and enabled in Claude. I asked Claude to give me
    • Zoho Books Product Road Map

      I am planning to look into Zoho Books to maintain my Company's Account Books. Is the roadmap of Zoho Books Development available online? What happens if you product dies, without an export feature to other popular Accounting softwares like Tally or Quickbook. Are we going to be left in lurch? Do you have a product road map? Regards, Vishal.
    • I want to delete the email but I can't.

      I want to delete emails but I can't, please help me. Thanks!
    • Error while creating new user

    • Zoho Mail is blacklisted on magicspam.com and spamauditor.org

      As of today, the same problem with the IP addresses 136.143.188.51 and 136.143.188.52 How long does it take them to clear their IP addresses? I've read on this forum that these IP addresses have been blacklisted for years. //////////////////////// This
    • Unable to send emails from the delegated mailbox

      currently it's not possible to send emails from our delegated mailbox (just in our own name, our own mail-accounts) The permissions granted include "Send as," and we are also unable to delete delegated employees or add new ones. We can only add employees
    • Change Password

      How can I reset OR Change the Passwords for the Whole Organization at Once as Administrator using Admin Console?
    • Zoho Books bill pay option not available with zoho one

      Why isn't Zoho Books bill pay add-on not available for Zoho one customers not even as a purchasable option. I think this is very inconvenient for companies wanting to use this feature all in one system
    • Support - what am I doing wrong?

      Hi Everyone - I'm a new user and looking particularly for a replacement mail service. I'm just a home user not a professional but I do look after half a dozen domains. Zoho looks lovely and I'd like to switch but just want to get answers to a few 'easy'
    • Zoho Forms - Form Availability Redirect Option

      Hi Forms Team, It would be great if there was a redirect URL option on the Form Availability settings. For example, I would like to create a support form which is only available outside business hours and if the current data and time is not Mon-Fri 9-5
    • Consider Making Printing Easier (UI)

      I'm using Zoho Analytics in much the way that it was intended when it was "Zoho Reports" - as a way to pull together information across several apps in the ecosystem. I have a dashboard that I need to run each week for every employee (change the filter
    • Email Routing to Zoho from Cloudflare

      Hello, I'm new to Cloudflare, having had my domain hosted on Fasthosts and just used it for email forwarding. I'm looking for a little help to configure my email routing/hosting. I've looked at the documentation but haven't found exactly what I need to
    • Emails

      Good Morning, Is anyone else experiencing emails bouncing back? Thanks,
    • Secondary Reporting Manager permission level

      Hi, same time last year I was speaking to Zoho about the use of a Secondary Reporting Manager and certain challenges that I was facing. at the time, I was told that this functionality did not exist. I am still facing challenges, so I would like to ask
    • why is my folder list is gone?

      Hi, All my email folders are gone, i cant found any email. could you please check it?
    • Our ZOHO mail is not working

      ZOHO mail is not working for any of our company's employees. All are able to access it in mobile. But not in laptops as a app or in webpage. please help asap.
    • Request to Release Email Aliases for Our Domain

      Dear Zoho Support Team, Greetings, We are currently configuring the email system for our organization in Zoho Mail and encountered an issue while creating several email aliases. When attempting to create these aliases, we receive the following error message:
    • Changing Base Currency to USD (we are based in the UAE)

      Hello everyone, Good day. We are based on the UAE and our bookkeeping is in USD. I followed the online resource that I select the country first where the currency I want as base is native then later on changed the country once the whole setup is done.
    • Error "The domain pinakajewels.com appears to be already in use in another organization. Learn More"

      Hi Team, I have the following account with zoho mail Organization ID: 644368849 This zoho mail account was earlier mapped to www.senacareese.com I no longer own the domain www.senacareese.com I wish to map www.pinakajewels.com to the same zoho mail account.
    • Add personal Facebook to Zoho Social

      Hi. is there any way i can post to my business and personal Facebook and Instagram at the same time when I make or schedule a post?
    • Option to Delete Chats in IM

      Currently, there is no option to delete any chats in IM, regardless of their source.
    • Billing Status Update

      Hello Latha, I’m working on a new automation (deluge) to fulfill one of our requirements. In this automation, there is a step to update the Work Order billing status from “Not Yet Invoiced” to “Non-Billable.” I tried to find the API information relevant
    • Next Page