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

    • Introducing the revamped What's New page

      Hello everyone! We're happy to announce that Zoho Campaigns' What's New page has undergone a complete revamp. We've bid the old page adieu after a long time and have introduced a new, sleeker-looking page. Without further ado, let's dive into the main
    • Picklist values out-of-date in Campaigns

      Hi I use a CRM (Global) picklist set of values for my field "Connection Strength" (see screenshot 1). . I use Campaigns to follow-up and change the Connection Strength value depending on their stage. The picklist values that appear in the dropdown for
    • Multiple Pipelines

      Is it possible to create multiple candidate pipelines?
    • Insert Template not inserting

      I have been using the "Insert Template" feature for years and I use it every single working day. Yesterday it was working fine. Today, on two different browsers (Chrome and Edge), I can select "Insert Template", select the template I want to insert, but
    • Default ticket template in helpcenter

      Hello, I have a web form and a ticket template created. How can I make that my default ticket template? If an user clicks New ticket or create a ticket, I want that template to be the default one. Thank you for the time and info.
    • 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
    • SalesIQ Email Delivery Issues to Microsoft

      Is anyone else having delivery issues to Hotmail, Outlook, and Live inboxes when sending transcripts and replies via email from SalesIQ? We’ve detected that emails sent from SalesIQ to these accounts aren't arriving—they don’t even bounce back; they simply
    • Zoho Commerce and Third-party shipping (MachShip) API integration

      We are implementing a third-party shipping (MachShip) API integration for our Zoho Commerce store and have made significant progress. However, we need guidance on a specific technical challenge. Current Challenge: We need to get the customer input to
    • Access images from form submission in power automate

      Images from form submission show up as links in power automate. How do I access the image data?
    • Need help to evaluate if Commerce is good for me

      Hi, I just want to quickly check if Zoho Commerce can fulfill my needs. Here is what I am looking for: - Multi-vendor plateform : We will be 3-4 different farms that will offer similar products (ex. tomatoes) to few selected customers (retaurants). All
    • Can't access google from toppings menu

      So... When I click the manage button in toppings, nothing happens. it won't let me access the settings.
    • 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
    • Ask the Experts: A Live Q&A Session

      Session Closed We've locked this post as the session has ended. We'll see you again in the next session! We’re back with another exciting edition of the Ask the Experts series, this time exclusively for our Zoho Recruit users from the USA & Canada regions!
    • T&C acceptance gate before estimate Accept, with audit trail

      We had to settle a Florida small-claims case in 2025 because we couldn't prove our customer was bound to the venue clause in our Terms & Conditions. The estimate footer mentioned the T&Cs, and Zoho Books logged the customer's IP and timestamp when they
    • Best sales insights for target accounts?

      Question for all the sales power-users out there: I would like to gain insights from Zoho CRM for a rotating list of target accounts. Each Outside Salesperson has 5 target accounts, and they can change these targets quarterly with management approval.
    • Contract to payment flow

      Hi everyone, I’m trying to set up a contract-to-payment flow and want to avoid duplicating invoices or customers in Zoho Books. The flow should be: contract generated from CRM, sent via Zoho Sign, client signs, deposit is paid, and the invoice should
    • Zoho Books | Product updates | May 2026

      Hello users, We're back with the latest updates and enhancements we've rolled out in Zoho Books. From sales tax automation to scanning receipts for free, explore the updates designed to upgrade your bookkeeping experience. Sales Tax Automation [US & Canada
    • Show backordered items on packing slip

      Is it possible to show a column on the Packing Slip that shows number of backordered items when a PO is only partially filled? I would also like to see the Backordered column appear on POs after you receive items if you didn't get ALL of the items or partial amounts of items. And lastly, it would be nice to have the option of turning on the Backordered column for invoices if you only invoice for a partial order. -Tom
    • Control Fields on Mobile App

      On the mobile app, how do we control which fields appear on the screen for records that have a related list? In the example below I want the Inspection Stage and Inspection Type fields to appear, not the record owner (Dev Admin). I changed the Inspections
    • 预期结果 实际结果 "zmverify.zoho.com" "zmverify.zoho.com."

      My domain is tenmokucup.com, I have a TXT record, but verification failed,Please help me, my TXT record is "zoho-verification=zb03390953.zmverify.zoho.com", I have added to DNS. You can confirm it. 预期结果 实际结果 "zmverify.zoho.com" "zmverify.zoho.com."
    • Adding options in the salutation drop down list (Books)

      Hello,  I am a new user still in the trial phase so I apologize if I have missed this. I did search the knowledge base and community first. I need to add a "Mr and Mrs" option in the salutation drop down options in Books. I have tried to find the edit
    • Zoho Desk - Event Calendar View

      Hi Desk team, Are there any plans to introduce a calendar or timeline view for Events in Zoho Desk? It would be very helpful if we could see Events visually in a calendar and/or timeline. This is very helpful when desk side support activities need to
    • Google Drive shared folder

      My deluge script has stopped working, no longer collecting files from Google Drive - have these connections finally been deprecated ?? They seem to be active but errors occur when updating them ?
    • All new Address Field in Zoho CRM: maintain structured and accurate address inputs

      Availability Update: 29 September 2025: It's currently available for all new sign-ups and for existing Zoho CRM orgs which are in the Professional edition exclusively for IN DC users. 2 March 2026: Available to users in all DCs except US and EU DC. 24
    • Journal Entries Do Not Show Multiple Entries to the Same Account

      Another basic accounting function that Books ... Accountants sometimes write journal entries, debiting and/or crediting the same account in the same entry. This is due to the need to record specific activity in an account when we pull reports especially
    • How to setup pricing in Zoho

      Hi everyone, I am relatively new here and have just moved from my old inventory system to the Zoho one. I am trying to get my head around how it all works. I am mostly setup connected to a shopify store, but I do manual sales also For manual invoicing,
    • Work Orders / Bundle Requests

      Zoho Inventory needs a work order / bundle request system. This record would be analogous to a purchase order in the purchasing workflow or a sales order in the sales cycle. It would be non-journaling, but it would reserve the appropriate inventory of
    • Windows agent 2026_M06 release notes

      Agent Version: 2026.03.19.0 Release date: 21 April, 2026 Major Enhancement: Connect as Individual Active users allowing you to connect to the same computer as different users in different tabs. Internal tool implementation to check for vulnerability in
    • Windows agent 2026_M05 release notes

      Agent Version: 111.0.3.335 Release date: 25 March, 2026 Major Enhancement: Quick Support feature compatibility code released. Code refactoring and removal of old code. Minor Enhancement: Updates to trackpad functionalities during remote session. Calendar
    • Windows agent 2026_M04 release notes

      Agent Version: 111.0.3.334 Release date: 10 March, 2026 Agent stickiness on multiple Windows Desktops to avoid confusion. Various other bug fixes and performance improvements.
    • 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
    • Can't add my domain to Zoho zeptomail because of error code DE_150

      Hello there, I'm trying to add my domain to ZeptoMail but it's rejected I checked the network tab and I'm getting { "error": { "code": "TM_3601", "details": [ { "code": "DE_150", "message": "Antispam validation failed for your domain in Accounts." } ],
    • Proforma Invoice

      Is there an option to create proforma and advance invoices in professional
    • 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
    • Windows agent 2026_M03 release notes

      Agent Version: 111.0.3.333 Release date: 19 February, 2026 Event Viewer Windows agent code release. Optimised the unattended agent uninstallation process to properly cleanup residual files and registry entries to prevent registry orphans.
    • Windows agent 2026_M02 release notes

      Agent Version: 111.0.3.332 Release date: 11 February, 2026 Code refactoring and removing of old unused code to optimise performance in specific versions of Windows Operating System.
    • Windows agent 2026_M01 release notes

      Agent Version: 111.0.3.331 Release date: 09 February, 2026 Enhancements and updates for improved Video Codec performance and stabler video rendering. Refactored legacy code for optimised performance with legacy devices. Upgrades to monitoring protocols
    • how to fix this error as soon as possible?

      how to fix this error as soon as possible? i have purchased it for monthly at 59₹ paid for an annum please let me know how to fix this issue as this one is urgent Server error: Message failed: 550 5.4.6 Unusual sending activity detected. Please try after
    • Transaction Rule Matching

      Q1. Is there a method to have the Transaction Rules applied across multiple Bank Accounts? Q2. To match "contains" text in Transaction Rules, is a wildcard or regular expression required? Is it case sensitive? Would searching for "issue" in the following
    • 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
    • Next Page