Skip to content

How to Create a Developer How-To with Runnable Write Examples for QBO & Xero

Learn to integrate QuickBooks, Xero, and NetSuite with a unified API. Runnable write examples, error handling, and an end-to-end invoice-to-payment quick start.

Nachi Raman Nachi Raman · · 14 min read
How to Create a Developer How-To with Runnable Write Examples for QBO & Xero

When enterprise procurement teams evaluate your B2B SaaS product, the real decision maker is rarely the person holding the budget. The true buyer is a lead architect or staff engineer who evaluates your platform by opening your documentation, finding a code snippet, and attempting to run it. If your API documentation is just a static Swagger dump, you will lose the technical evaluation.

This is especially true for accounting integrations. Writing data to QuickBooks Online (QBO), Xero, and NetSuite is notoriously difficult. These APIs enforce strict double-entry accounting rules, complex tax code mappings, and aggressive rate limits. If an evaluating engineer has to spend three hours reverse-engineering undocumented payloads just to create an invoice, they will abandon the integration.

To reduce friction and accelerate adoption, your product team must provide runnable, copy-pasteable write examples. This guide provides a concrete framework for writing developer tutorials that handle the painful realities of QBO, Xero, and NetSuite, ensuring your users get to a successful 200 OK in under five minutes. It also includes an end-to-end quick start showing how to create invoices and apply payments through a single unified API endpoint that works across all three providers.

Why Time to First Call (TTFC) Dictates Accounting API Adoption

Time to First Call (TTFC) is a developer experience metric that measures the elapsed time from a developer signing up for your service to executing their first successful, authenticated API request that returns a non-error response.

Accounting APIs are not simple key-value stores. Creating a valid invoice in QBO requires referencing existing customer IDs, mapping correct tax rates, and ensuring line items balance perfectly. If you leave developers to figure this out on their own, your TTFC will be measured in days, not minutes.

Industry data proves that providing runnable API examples directly impacts adoption. According to Postman's research on API publishers, providing a pre-configured, runnable collection can improve a developer's TTFC by 1.7x to 56x. When developers can instantly see how a payload is structured and watch it execute against a sandbox environment, they skip hours of trial and error.

If you want to learn more about structuring these tutorials at a high level, read our guide on how to publish an end-to-end developer tutorial with API examples. For this guide, we will focus specifically on the technical hurdles of writing data to accounting ledgers.

The Challenges of Writing Data to QBO and Xero

Before you write your tutorial, you must understand the specific technical hurdles your developers will face when interacting with these two platforms. Your code examples must explicitly solve these problems.

Xero's Strict Rate Limits and Egress Pricing

Xero enforces strict rate limits to protect their infrastructure. The limits are hardcoded at 60 calls per minute and 5,000 calls per day per organization. If a developer attempts to bulk-create 100 invoices in a simple for loop, Xero will immediately return HTTP 429 Too Many Requests errors, breaking the integration.

Furthermore, Xero is fundamentally changing its developer economics. In March 2026, Xero is shifting to a tiered pricing model based on API usage and egress data allotments. According to reports from Accounting Today, developers will face overage charges if they exceed their egress GB limits. This makes efficient API calls mandatory. Your tutorials cannot recommend polling or inefficient bulk writes; they must demonstrate batched operations and intelligent caching.

QBO's Strict Technical Review for UI Accuracy

Intuit (the parent company of QuickBooks) maintains a notoriously strict App Assessment process. Writing data to the QBO API is not enough - the data must look correct to the end user inside the QBO graphical interface.

Intuit's technical requirements mandate that any app writing data to QBO must pass a review to verify UI accuracy. If your developer creates an invoice via the API, but the tax rounding is off by one cent, or the line items do not map to the correct income accounts, Intuit will reject the application. Your tutorial must provide exact, validated JSON payloads that are guaranteed to pass this UI review.

The Complexity of References and Line Items

Both APIs require deep relational mapping. You cannot simply pass a string for a customer name. You must first query the API to find the Customer ID, query again to find the Item ID, query a third time to find the Tax Code ID, and then construct a nested JSON payload containing all of these references. Your runnable examples must demonstrate this multi-step orchestration.

How to Create a Developer How-To with Runnable Write Examples

To build a tutorial that actually converts evaluating engineers into active users, follow this four-step framework.

Step 1: Abstract the Authentication Boilerplate

OAuth 2.0 authorization code flows are the death of TTFC. If your tutorial starts with "First, build an OAuth callback handler to exchange your authorization code for a bearer token," you have already lost the reader.

Your runnable examples should assume the developer already has a valid access token. If you are building the integration infrastructure in-house, provide a CLI tool or a developer portal button that generates a temporary sandbox token.

If you are using a unified API platform like Truto, authentication is handled entirely outside of the developer's code. Truto proactively refreshes OAuth tokens with a 30-second buffer before expiry, meaning the developer never has to write token refresh logic. Your tutorial can simply instruct them to pass their Truto API key and the Integrated Account ID.

Step 2: Provide Deterministic, Copy-Pasteable JSON Payloads

Do not provide abstract schema definitions. Provide real, hardcoded JSON payloads that will execute successfully against a standard sandbox environment.

Here is an example of what a raw Xero Invoice payload looks like in a high-quality tutorial. Notice how it explicitly comments on the required reference IDs.

{
  "Invoices": [
    {
      "Type": "ACCREC",
      "Contact": {
        "ContactID": "00000000-0000-0000-0000-000000000000" // Replace with a valid ContactID
      },
      "LineItems": [
        {
          "Description": "Annual SaaS Subscription",
          "Quantity": 1.0,
          "UnitAmount": 1200.00,
          "AccountCode": "200", // Must match an existing revenue account in Xero
          "TaxType": "OUTPUT"
        }
      ],
      "Date": "2026-10-01",
      "DueDate": "2026-10-31",
      "Status": "AUTHORISED"
    }
  ]
}

Step 3: Demonstrate Idempotency

Financial data requires strict idempotency. If a network timeout occurs during an invoice creation, the developer's code might retry the request, resulting in duplicate invoices and ruined financial reporting.

Your runnable examples must show how to use idempotency keys. In Xero, this is often handled by passing a unique InvoiceNumber. If the invoice number already exists, Xero returns a validation error rather than creating a duplicate. In your code samples, explicitly demonstrate how to generate and pass these unique identifiers.

Step 4: Include Runnable Request Code with Error Handling

A bare curl command is not enough. Provide a complete script in Node.js or Python that makes the request, parses the response, and handles the specific errors the accounting API is likely to throw.

// Example: Writing an Invoice to Xero with basic error parsing
async function createXeroInvoice(accessToken, tenantId, invoicePayload) {
  const response = await fetch('https://api.xero.com/api.xro/2.0/Invoices', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Xero-tenant-id': tenantId,
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(invoicePayload)
  });
 
  if (response.status === 429) {
    const retryAfter = response.headers.get('Retry-After');
    throw new Error(`Rate limited by Xero. Retry after ${retryAfter} seconds.`);
  }
 
  if (!response.ok) {
    const errorData = await response.json();
    console.error("Xero Validation Failed:", JSON.stringify(errorData, null, 2));
    throw new Error("Failed to create invoice. Check payload references.");
  }
 
  const data = await response.json();
  return data.Invoices[0];
}

Handling Xero Rate Limits and QBO Errors in Your Code Samples

Your tutorials must prepare developers for production realities. Failing to document how to handle HTTP 429s and validation errors is a severe disservice to your users.

Architecting Exponential Backoff for Xero

Because Xero enforces a strict 60 requests per minute limit, any script that bulk-syncs data will eventually hit a wall. Your tutorial must include a section on exponential backoff.

If you are routing requests through Truto, the platform normalizes upstream rate limit information into standardized headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset) following the IETF specification.

Warning

Important Architectural Note: Truto does not automatically retry, throttle, or apply backoff on rate limit errors. When Xero or QBO returns an HTTP 429, Truto passes that error directly to the caller. The caller is strictly responsible for inspecting the ratelimit-reset header and implementing their own retry logic.

Provide a runnable backoff wrapper in your tutorial so developers do not have to write it from scratch. You can read more about this pattern in our guide on best practices for handling API rate limits and retries.

sequenceDiagram
    participant Developer App
    participant Truto Proxy
    participant Xero API
    
    Developer App->>Truto Proxy: POST /proxy/xero/Invoices
    Truto Proxy->>Xero API: POST /api.xro/2.0/Invoices
    Xero API-->>Truto Proxy: 429 Too Many Requests
    Truto Proxy-->>Developer App: 429 (with ratelimit-reset header)
    Note over Developer App: Developer code pauses execution<br>based on ratelimit-reset value
    Developer App->>Truto Proxy: Retry POST /proxy/xero/Invoices
    Truto Proxy->>Xero API: POST /api.xro/2.0/Invoices
    Xero API-->>Truto Proxy: 200 OK
    Truto Proxy-->>Developer App: 200 OK

Handling QBO Stale Object Errors

QuickBooks Online uses a SyncToken system for optimistic concurrency control. Whenever you update a record in QBO, you must provide the current SyncToken. If another process has updated the record since you last fetched it, the SyncToken will not match, and QBO will throw a Stale Object Error.

Your write examples must demonstrate the read-before-write pattern required by QBO. Instruct the developer to fetch the target invoice, extract the SyncToken, apply their updates to the payload, and then execute the POST request.

Scaling Your Tutorials with a Unified Accounting API

Writing a dedicated, end-to-end tutorial for Xero is time-consuming. Writing a second one for QuickBooks Online is frustrating. Writing a third for NetSuite is a massive drain on your engineering resources.

Every accounting platform has entirely different payload structures. Xero calls them ContactID, QBO calls them CustomerRef, and Sage calls them contact_id. If you force your developers to integrate point-to-point, you must maintain separate documentation, separate tutorials, and separate mock payloads for every single provider.

This is why modern B2B SaaS companies use a unified API architecture. A unified API abstracts the differences between QBO, Xero, NetSuite, and Sage into a single, standardized JSON schema.

graph TD
    A[Developer App] -->|Standardized Unified JSON| B(Unified Accounting API)
    B -->|Translates to QBO XML/JSON| C[QuickBooks Online]
    B -->|Translates to Xero JSON| D[Xero]
    B -->|Translates to SuiteQL| E[NetSuite]

Instead of writing ten different tutorials, you write one. You show the developer how to POST a unified Invoice object to the /unified/accounting/invoices endpoint. The unified API engine handles the provider-specific field mapping, date formatting, and authentication injection.

If you are building an integration layer for your product, you should read our guide on how to build a developer cookbook for unified accounting APIs to see exactly how to structure this documentation.

By normalizing the data model, you drastically reduce the cognitive load on the evaluating engineer. They do not need to learn Xero's idiosyncrasies or QBO's SyncToken logic. They learn your unified schema once, and their code instantly works across every accounting provider you support. For a deeper dive into this architecture, review our analysis of the best unified accounting API for B2B SaaS.

Quick Start: Create an Invoice and Apply a Payment (End-to-End)

The sections above describe the principles. Here is what using a single API for QuickBooks, Xero, and NetSuite integration actually looks like in practice. Every request below hits the same endpoint with the same payload shape - the integrated_account_id parameter determines which accounting provider receives the call.

Base URL: https://api.truto.one

Create the Invoice

POST /unified/accounting/invoices?integrated_account_id=ia_abc123
Content-Type: application/json
Authorization: Bearer truto_sk_xxxx
 
{
  "contact_id": "con_98234",
  "invoice_type": "invoice",
  "issue_date": "2026-10-01",
  "due_date": "2026-10-31",
  "currency": "USD",
  "line_items": [
    {
      "description": "Annual SaaS Subscription",
      "quantity": 1,
      "unit_amount": 1200.00,
      "account_id": "acc_401",
      "tax_rate_id": "tax_10"
    }
  ]
}

Response:

{
  "result": {
    "id": "inv_50412",
    "contact_id": "con_98234",
    "invoice_type": "invoice",
    "status": "OPEN",
    "issue_date": "2026-10-01",
    "due_date": "2026-10-31",
    "total": 1320.00,
    "currency": "USD",
    "line_items": [
      {
        "description": "Annual SaaS Subscription",
        "quantity": 1,
        "unit_amount": 1200.00,
        "account_id": "acc_401",
        "tax_amount": 120.00
      }
    ],
    "remote_data": {
      "Id": "50412",
      "SyncToken": "0",
      "TotalAmt": 1320.00,
      "Balance": 1320.00,
      "CustomerRef": { "value": "98234", "name": "Acme Corp" }
    }
  }
}

This single request works whether ia_abc123 points to QuickBooks Online, Xero, or NetSuite. The unified API translates your payload into the provider's native format, sends it, and maps the response back to a standardized schema. The remote_data field in this example shows QBO's native fields - if the account pointed to Xero, you would see Xero-native fields like InvoiceID and Type: "ACCREC" instead.

Apply a Payment

POST /unified/accounting/payments?integrated_account_id=ia_abc123
Content-Type: application/json
Authorization: Bearer truto_sk_xxxx
 
{
  "contact_id": "con_98234",
  "invoice_id": "inv_50412",
  "amount": 1320.00,
  "date": "2026-10-15",
  "currency": "USD"
}

Response:

{
  "result": {
    "id": "pay_7721",
    "contact_id": "con_98234",
    "invoice_id": "inv_50412",
    "amount": 1320.00,
    "date": "2026-10-15",
    "currency": "USD",
    "remote_data": { "Id": "7721", "TotalAmt": 1320.00, "PaymentRefNum": "TRUTO-7721" }
  }
}

Verify the Invoice Is Paid

GET /unified/accounting/invoices/inv_50412?integrated_account_id=ia_abc123
Authorization: Bearer truto_sk_xxxx

Response:

{
  "result": {
    "id": "inv_50412",
    "status": "PAID",
    "total": 1320.00,
    "amount_due": 0.00,
    "remote_data": {
      "Id": "50412",
      "SyncToken": "1",
      "Balance": 0.00
    }
  }
}

Inspect Provider-Native Fields via remote_data

Every unified response includes remote_data containing the raw, unmodified response from the underlying provider. This matters when your application needs a field that only exists in one provider's API.

For a QBO-connected account, remote_data on an invoice includes fields like SyncToken, MetaData.LastUpdatedTime, and CustomerRef. For a Xero-connected account, the same unified invoice response would instead contain InvoiceID, InvoiceNumber, HasAttachments, and Contact.ContactID inside remote_data. NetSuite accounts would show custbody* custom fields and tranId.

The unified fields (id, status, total, line_items) are identical across all three providers. The provider-specific fields live exclusively inside remote_data, so your core integration code never branches on the provider.

If you want smaller payloads and do not need provider-native fields, add truto_ignore_remote_data=true to your query string.

Recover from a Bad Write

Suppose the contact_id you referenced does not exist in the target ledger. The unified API returns the provider's error with enough context to diagnose the problem:

HTTP/1.1 422 Unprocessable Entity
{
  "error": {
    "status": 422,
    "message": "Validation failed",
    "details": {
      "provider_error": "Object Not Found: Something you're trying to use has been made inactive. Check the fields with accounts, customers, items, vendors or employees.",
      "provider_status": 400
    }
  }
}

The fix: re-query /unified/accounting/contacts to get a valid contact_id, update your payload, and retry. The error shape is consistent regardless of which provider rejected the write.

That is five calls - create, pay, verify, inspect, and handle failure - and the same code works across QuickBooks Online, Xero, and NetSuite without changing a single field name.

Error Handling and Recovery for Failed Writes

Accounting APIs enforce double-entry rules, require valid references, and reject writes that violate business logic. When writing through a unified API, provider errors are normalized into a consistent structure. Here are the most common failure modes and how to recover from each.

Validation Errors (HTTP 422)

If your payload references a contact ID, account ID, or tax rate that does not exist in the target ledger, the provider rejects the write. The unified API surfaces this as an HTTP 422:

{
  "error": {
    "status": 422,
    "message": "Validation failed",
    "details": {
      "provider_error": "Invalid reference: AccountRef 'acc_999' not found",
      "provider_status": 400
    }
  }
}

Recovery: Re-query the referenced entity (contact, account, item) to confirm it exists and is active. The most common cause is a stale ID from a cached lookup. If you cache reference data locally, set a TTL of 15-30 minutes and re-fetch on any 422.

Stale Object Errors (HTTP 409)

QuickBooks Online's SyncToken concurrency model means an update can fail if someone else modified the record between your read and your write. The unified API maps this to HTTP 409:

{
  "error": {
    "status": 409,
    "message": "Stale object",
    "details": {
      "provider_error": "Stale Object Error: You and another user were working on this at the same time.",
      "provider_status": 400
    }
  }
}

Recovery: Fetch the record again to get the latest state, re-apply your changes, and retry. Wrap updates in a read-modify-write loop capped at 2-3 retries. This pattern is QBO-specific - Xero and NetSuite use different concurrency models - but the unified API error structure is the same across all providers.

Rate Limit Errors (HTTP 429)

Xero enforces 60 calls per minute per organization. QBO has its own per-app and per-realm limits. The unified API passes through 429 responses with standardized rate limit headers:

HTTP/1.1 429 Too Many Requests
ratelimit-limit: 60
ratelimit-remaining: 0
ratelimit-reset: 42

Recovery: Read ratelimit-reset (seconds until the window resets) and wait before retrying. For bulk operations, pre-calculate your call budget based on ratelimit-remaining and pace requests accordingly.

General Recovery Pattern

For any failed write against the unified accounting API, follow this sequence:

  1. Check the HTTP status code to classify the error (422 = validation, 409 = conflict, 429 = rate limit, 5xx = server error)
  2. Read error.details.provider_error for the provider's original message
  3. For 422: fix the payload references and retry once
  4. For 409: re-fetch the record, re-apply changes, and retry with fresh state
  5. For 429: wait for ratelimit-reset seconds, then retry
  6. For 5xx: retry with exponential backoff (1s, 2s, 4s) up to 3 attempts
  7. Log the full error response - including provider_status and provider_error - for debugging

Synchronous vs Asynchronous Writes: What to Expect

When you POST to a unified accounting endpoint, the write is synchronous. Your HTTP request stays open while the platform translates your unified payload into the provider-native format, sends it to QuickBooks, Xero, or NetSuite, waits for the provider's response, maps it back to the unified schema, and returns the result.

This means:

  • A successful response confirms the record was created in the provider's system. There is no eventual consistency delay.
  • The response body contains the provider-assigned ID, server-computed fields (like tax totals), and full remote_data.
  • Typical response times range from 200ms to 2 seconds, depending on the provider. NetSuite tends to be the slowest due to its governance model. Xero is usually the fastest for single-record writes.

There is no background queue for write operations. If the provider is down or the request times out, you get an error immediately and can decide how to handle it.

For high-volume write scenarios - syncing hundreds of invoices or expenses - the synchronous model means you manage concurrency yourself. Practical limits:

  • Xero: Cap at 50 requests per minute to stay safely under the 60/min limit
  • QBO: Use 5-10 concurrent requests, scoped per realm (each connected QuickBooks company)
  • NetSuite: Limit to 2-4 concurrent requests per account due to SuiteQL governance limits

If you need to push large volumes, consider batching operations and spreading them across time windows rather than firing hundreds of synchronous calls at once.

Strategic Next Steps for DevRel and Product Teams

Your API documentation is a sales asset. If your tutorials are difficult to run, evaluating engineers will recommend your competitors. To fix your TTFC, audit your current documentation today. Find your most critical write operations (like creating an invoice or syncing an expense) and rewrite the documentation to include hardcoded, copy-pasteable JSON payloads and robust error-handling scripts.

Stop relying on generated Swagger docs to do the heavy lifting. Abstract the authentication, explicitly document the rate limit headers, and prove to the developer that your platform respects their time.

FAQ

What is Time to First Call (TTFC) in API integrations?
Time to First Call (TTFC) is a metric measuring the time from a developer signing up for an API to executing their first successful, authenticated request. It is a critical indicator of developer experience and onboarding friction.
How do I handle Xero API rate limits in my code?
Xero enforces a strict limit of 60 calls per minute. Your code must inspect the rate limit headers returned in HTTP 429 responses and implement an exponential backoff strategy to pause execution before retrying.
Why does QuickBooks Online reject API write requests?
QBO often rejects requests due to missing reference IDs, unbalanced line items, or Stale Object Errors caused by mismatched SyncTokens. Additionally, Intuit requires strict technical reviews to ensure data written via the API renders correctly in their UI.
Does Truto automatically retry failed API calls?
No. Truto normalizes upstream rate limit information into standardized IETF headers but passes HTTP 429 errors directly to the caller. The developer is responsible for implementing their own retry and backoff logic.

More from our Blog