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 SlyteUI : From Idea to a Working Interface in Minutes

        Hello everyone! Are you spending hours building basic UIs? Does even the smallest customization feel like a major task? CRM customization should feel intuitive and straightforward, not time consuming or exhausting. SlyteUI makes this possible by simplifying
      • Scale up your support with Zia actions

        Hi there, Have you explored how Zia can help automate repetitive tasks? In customer support, every detail matters when delivering the right solutions to customers, and Zia is here to support you. This edition highlights how Zia can simplify and streamline
      • Disable Zoho Contacts

        We don't want to use this app... How can we disable it?
      • Zoho CRM with Built-In MCP Support

        Zoho CRM now provides built-in support for Model Context Protocol (MCP), enabling AI tools to connect and perform CRM actions using natural language. Zoho CRM MCP servers act as a bridge between AI agents and Zoho CRM, exposing CRM capabilities as callable
      • Allow Zia Agents using Zoho One Account

        When I went to try Zia Agents, it forced the creation of a new, separate account. This seems counter-intuitive to me. I don't want to manage a separate account. Plus, aren't Zoho One users the ones most likely to adopt Zia Agents most quickly? It seems
      • My Zoho mail stopped receiving or sending emails about 3 hours ago

        Its a pop 3 account. The emails get into the actual mailbox on the server and I can send emails directly from the server, but they are no longer in Zoho, in neither of my Zoho accounts. All green ticks under Mail Accounts under Settings
      • POP mailbox limits

        If I am accessing a remote POP mail server using Zoho Mail is there a mailbox quota for the account or is it all related to my mail account storage limits?
      • SPF: HELO does not publish an SPF Record

        I am using Zoho mail. Completed all of the required prerequisites from the dashboard to avoid any issues with mail delivery. But when checking on mail-tester.com getting the following error. Can anyone help me solve this?
      • Check out in Meetings

        Why there is no check out in Meetings of Zoho CRM, very difficult to track
      • Ability to Attach Images When Reporting Issues to Zoho Projects from Zoho Desk

        Hi Zoho Desk Team, Hope you’re doing well. We’re using the Zoho Desk–Zoho Projects integration to report bugs directly from support tickets into the Zoho Projects issue tracker. This integration is extremely useful and helps us maintain smooth coordination
      • Zoho Show Keeps Crashing and Goes Down with the Data

        Today, I am experiencing something unusual: Zoho Show keeps crashing and goes down with the data. When you try to reopen the same deck, you get that lovely "Presentation not found" error. This has happened today rather frequently with multiple decks.
      • Outgoing Mail Blocked – Suspicious Login Activity (Need Clarification and Solution)

        Hello, I’m currently facing an issue where my Zoho Mail account has been blocked due to “suspicious login activity,” and outgoing emails are restricted. Here are the details shown: Block type: Outgoing mail blocked Reason: Suspicious login activity A
      • Escalate the tickets to the escalation team through email

        Hi Zoho Team, I would like to ask if it is possible to escalate the tickets when the status changed to "Escalated" by sending an email to them. I have attached the snapshot from my Zoho desk where I want it to email to a certain group(escalation team).
      • Mailbox storage showing incorrect usage

        My mailbox shows 4.99 GB used out of 5 GB. However, actual mailbox usage is only around 394 MB. Trash and Spam are already empty. IMAP/POP is not enabled. WorkDrive is not in use. This appears to be a storage calculation issue. Please help to recalculate
      • Google Fonts Integration in Pagesense Popup Editor

        Hello Zoho Pagesense Team, We hope you're doing well. We’d like to submit a feature request to enhance Zoho Pagesense’s popup editor with Google Fonts support. Current Limitation: Currently, Pagesense offers a limited set of default fonts. Google Fonts
      • Have to ask: WHY is Zoho Purposefully blocking Apple Reminders?

        I just have to ask: Why is zoho purposefully blocking Apple reminders (CalDav). This is just strange... I just can't see a reason why this is done both within the Zoho calendar and mail calendar? These are both paid services. Why? This little simple feature is forcing me to leave Zoho. Our workflow depends on Reminders and after talking with an Apple engineer, they noticed theat Zoho is purposefully blocking this caldav port call. I just don't understand why. Thanks!
      • I am not able to check in and checkout in zoho people even location access allowed

        This issue i am facing in mackbook air m1, I allowed location in chrome browser and i also tried in safari but getting similar issue. Please have a look ASAP.
      • Issue with attaching files to a task through the API

        Hello! I've implemented a function that creates a task for every new bill that is created but I haven't been able to attach to the task the files which are attached to the bill. I have encountered multiple errors but the most common one is error 33003:
      • Printing receipts with 80mm thermal printer and Zoho Creator

        I have a sales form in my Zoho Creator app and I would like to print receipts for customers. Question 1: From what I've read on this forum there's no way to submit and have the receipt print automatically, you'd have to go through the printer dialog,
      • Integrate with WooCommerce using Wordpress Plugin

        We’re thrilled to announce a powerful update to the Zoho Marketing Automation WordPress plugin with WooCommerce integration! This enhancement enables new possibilities for businesses running online stores using WooCommerce, empowering them to merge seamless
      • Removing To or CC Addresses from Desk Ticket

        I was hoping i could find a way to remove unnecessary email addresses from tickets submitted via email. For example, a customer may email the support address AND others who are in the helpdesk notification group, in either the TO or CC address. This results
      • When I schedule calendar appointments in zoho and invite external emails, they do not receive invites

        Hello, We have recently transitioned to zoho and are having a problem with the calendar feature. When we schedule new calendar appointments in zoho the invite emails aren't being sent to the external users that we list in participants. However, this works
      • Sync desktop folders instantly with WorkDrive TrueSync (Beta)

        Keeping your important files backed up and accessible has never been easier! With WorkDrive desktop app (TrueSync), you can now automatically sync specific desktop folders to WorkDrive Web, ensuring seamless, real-time updates across devices. Important:
      • Unable to send

        Hello, I am unble to send any single email during the whole time due to the Zoho IP 136.143.188.16 being bloked by SpamCop.net Please help can somebody help me?
      • Errorcode 554

        Hello, I am unble to send any single email during the whole time due to the Zoho IP 136.143.188.16 being blocked by SpamCop. Please can somebody help me?
      • Spamcop

        Have been trying to email several of our clients and many of our emails keep getting bounced back with an error that states: ERROR CODE :550 - "JunkMail rejected - sender4-op-o16.zoho.com [136.143.188.16]:17694 is in an RBL: Blocked - see spamcop.net/bl.shtml?136.143.188.16"
      • Emails being blocked / spamcop

        Hello, I am unablr to send any single email during the whole time due to the Zoho IP 136.143.188.16 being blocked by SpamCop.net Please help on this.
      • Zoho IP blocked by SpamCop 136.143.188.16

        Hello, I am unble to send any single email during the whole time due to the Zoho IP 136.143.188.16 being blocjed by SpamCop.net Please help on this.
      • This domain is not allowed to add in Zoho. Please contact support-as@zohocorp.com for further details

        After not using the domain rcclima.org.pe for some time, I'm trying to set up the free version of Zoho Mail. When I tried to validate my domain, rcclima.org.pe, I received the error message discussed in this thread: "This domain is not allowed to be added
      • 554 5.1.1 – Mail sending blocked for the domain(s): [gmail.com]

        Here's your corrected text: Hello, I hope you are doing well. I was unable to send a message and received the following error: "554 5.1.1 – Mail sending blocked for the domain(s): [gmail.com]" I tried to send and deliver an email but got this error. I
      • Can I hide empty Contact fields from view?

        Some contacts have a lot of empty fields, others are mostly filled. Is there a way I can hide/show empty fields without changing the actual Layout? I would like to de-clutter my view, and also be able to add information later as I am able. I would be
      • Replying from same domain as a catch-all?

        I have 2 domains setup on Zoho, both with associated email addresses. They look something like this: john@example.com (primary address) john@test.com (this domain also has a catch-all setup) I use the catch-all for test.com as a public-facing email address
      • Account and Email and Password

        I'm signing up as a Partner so I can move my website clients across to a separate email server from their current cPanel one.. So I have a Zoho account and then I moved one of my emails across to that account to test the import process... So the question
      • Zoho CRM Copilot Connector

        Hello, Are there plans to release a connector for Zoho CRM and Copilot? I'm in the early research stages of potentially switching our CRM solution to Microsoft Dynamics because of its out of the box integration with Copilot. The advantage being that we
      • Approval Workflow for Purchase Orders Abrir

        The requirement is , that all purchase orders greater than or equal to 5000 go through an approval process from certain people, but within books I only see that the approvers can be by levels or any approver but we cannot enter a rule like these. Can
      • Zoho mail admin panel not opening

      • Cloning Module Customizations in Zoho FSM

        Hello Latha, I have two Zoho FSM accounts, each in a different data center. I would like to know if it is possible to clone the module customizations I have already completed in one account to my new account in the UAE from the backend. In other words,
      • is it possible to adjust the date field to show Monday as a first day

        Hi, Is it possible to adjust somewhere the date field, so the first day of the week is Monday (instead of Sunday)? Thank you! Ferenc
      • Create your own Zoho CRM homepage using Canvas - Home View!

        Hello Partners We are excited to share a new enhancement to Canvas in Zoho CRM that gives users more control over how their homepage looks and works. Introducing Canvas Home View! What is Canvas Home View? You can now create and design a custom homepage
      • can change deal colors in bigin?

        can be super useful if we can change colors deals process any way to do it?
      • Next Page