VOOZH about

URL: https://www.apideck.com/blog/rillet-api-integration-2026.md


--- title: "How to Integrate with the Rillet API in 2026" description: "A practical guide to integrating with the Rillet API in 2026: authentication, pagination, idempotency, webhooks, incremental sync, financial reports, and the new MCP server for AI coding tools." author: "GJ" published: "2026-03-11T00:00+00:00" updated: "2026-03-31T12:46:20.137Z" url: "https://www.apideck.com/blog/rillet-api-integration-2026" category: "Accounting" tags: ["Accounting", "Guides & Tutorials"] --- # How to Integrate with the Rillet API in 2026 Rillet has quietly become one of the more interesting accounting platforms for SaaS and fintech companies. Built on REST principles, following the OpenAPI Specification, and recently shipping an MCP server — it's a system that's clearly designed by people who have actually integrated with legacy ERPs and didn't want to recreate that pain. This guide covers everything you need to get up and running with the Rillet API: authentication, pagination, idempotency, webhooks, error handling, and the newer MCP server setup. Whether you're syncing financial data from a payment processor, building a custom integration between Rillet and your internal systems, or using an AI coding agent to accelerate development, this should give you a solid foundation. If you'd rather skip the direct integration work, [Apideck now supports Rillet](https://www.apideck.com/integrations/rillet) as a connector through the [Unified Accounting API](https://www.apideck.com/accounting-api). That means you can read and write Rillet data through the same API you'd use for QuickBooks, Xero, NetSuite, or any of 30+ other accounting platforms. More on that at the end of this guide. --- ## What the Rillet API Actually Is Before jumping into code, it's worth understanding the scope. The Rillet API is a REST API that gives you programmatic access to everything a user can do in the UI: customers, invoices, payments, credit memos, journal entries, chart of accounts, reports (balance sheet, trial balance), and more. It's versioned, paginated, and follows RFC 9457 for structured error responses — which is a good sign. The API spec is available as an OpenAPI download at `https://docs.api.rillet.com/openapi`, which means you can generate client code in any language you prefer without hand-rolling everything. --- ## Environments Rillet provides two environments: - Production: `https://api.rillet.com` - Sandbox: `https://sandbox.api.rillet.com` Start in sandbox. The sandbox mirrors production behavior, so anything you build there will work in production without surprises. All examples in this guide use the sandbox URL. --- ## Authentication Rillet uses API key authentication. To get a key, contact your Rillet team to enable API access, then create and manage keys in your Organization Settings under API Access. Every request needs the key in the `Authorization` header as a Bearer token: ```bash curl --request GET \ --url https://sandbox.api.rillet.com/customers \ --header 'Authorization: Bearer YOUR_API_KEY' ``` That's it. No OAuth dance, no token expiry to manage. Keep your key in an environment variable and never hardcode it. ```javascript const RILLET_API_KEY = process.env.RILLET_API_KEY; const headers = { 'Authorization': `Bearer ${RILLET_API_KEY}`, 'Content-Type': 'application/json', }; ``` --- ## API Versioning The API is versioned, and you should always target a specific version explicitly rather than relying on the default (which falls back to v1.0). Pass the version in an HTTP header: ```bash curl --request GET \ --url https://sandbox.api.rillet.com/customers \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'X-Rillet-API-Version: 3' ``` At the time of writing, v3 is the current version. Pinning your version header means you won't get unexpectedly broken by a major API update. Check the docs for the exact format — the changelog indicates versions are referenced as integers (`2`, `3`), not decimals. --- ## Pagination All list endpoints use keyset-based pagination, which is the right choice for financial data where consistent page sizes and response times matter more than offset-based approaches. Responses come back in reverse chronological order and include a `pagination` object: ```json { "data": [...], "pagination": { "next_cursor": "VdW1ptsZbOB4E1fq" } } ``` To fetch the next page, pass the cursor as a query parameter: ```bash curl "https://sandbox.api.rillet.com/invoices?cursor=VdW1ptsZbOB4E1fq" ``` A few things to know: - Cursors are valid for **2 hours**. If yours expires, a new pagination sequence starts from the first page. - The default page size is 25. You can set it with the `limit` parameter, up to a maximum of 100. - When there's no `next_cursor` in the response, you've reached the last page. Here's a simple function to paginate through all invoices: ```javascript async function getAllInvoices() { const invoices = []; let cursor = null; do { const url = new URL('https://sandbox.api.rillet.com/invoices'); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('cursor', cursor); const response = await fetch(url, { headers }); const data = await response.json(); invoices.push(...data.data); cursor = data.pagination?.next_cursor ?? null; } while (cursor); return invoices; } ``` --- ## Incremental Sync with `updated_at` Filters Rillet has been rolling out `updated_at` timestamps and `updated.gt` filter support across its entities. As of February 2026, accounts and custom fields explicitly gained both the timestamp field and the filter parameter. Other entities — customers, invoices, payments, credit memos, and journal entries — already carried `updated_at` in responses, though filter support varies by entity; check the changelog and OpenAPI spec for what's available on each. For accounts specifically, confirmed as of February 2026: ```bash curl "https://sandbox.api.rillet.com/accounts?updated.gt=2026-02-01T00:00:00Z" \ --header 'Authorization: Bearer YOUR_API_KEY' ``` For reporting journal entries, the endpoint also supports `updated.gt` filtering: ```bash curl "https://sandbox.api.rillet.com/reports/journal-entries?updated.gt=2026-02-01T00:00:00Z" \ --header 'Authorization: Bearer YOUR_API_KEY' ``` The general pattern — poll periodically, store the last sync timestamp, fetch only what's changed — is the right approach for building a sync pipeline. Before assuming `updated.gt` works on a given entity, verify it against the current OpenAPI spec at `https://docs.api.rillet.com/openapi`. --- ## Idempotency For POST requests (creating invoices, customers, journal entries, etc.), Rillet supports idempotency keys. This is critical if you're dealing with network failures or retries — you don't want to double-create an invoice because a request timed out. Pass a unique key in the `Idempotency-Key` header: ```bash curl --request POST \ --url https://sandbox.api.rillet.com/invoices \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Idempotency-Key: f0e9a51e-905d-4caf-a5dc-64d326574646' \ --header 'Content-Type: application/json' \ --data '{"customer_id": "cust_123", ...}' ``` A few rules: - Use UUID v4 for your keys — high entropy, low collision risk. - The same response is returned for 24 hours after the first successful request. After that, the same key will create a new object. - If a request fails due to a validation error, the response isn't saved and you can safely retry with the same key. - If a second request arrives while the first is still processing, you'll get a `409 Conflict`. --- ## Rate Limiting The limit is 60 requests per rolling minute. Requests over that threshold return HTTP `429`. Build in retry logic with exponential backoff: ```javascript async function rilletRequest(url, options, retries = 3) { for (let attempt = 0; attempt < retries; attempt++) { const response = await fetch(url, { ...options, headers }); if (response.status === 429) { const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s await new Promise(resolve => setTimeout(resolve, delay)); continue; } return response; } throw new Error('Rate limit exceeded after retries'); } ``` At 60 requests per minute, you're unlikely to hit this in normal operations unless you're running a bulk sync. If you are, batch your calls and space them out accordingly. --- ## Error Handling Rillet follows RFC 9457 for structured error responses. A `4xx` error looks like this: ```json { "type": "https://rillet.com/illegal-argument", "title": "Bad Request", "status": 400, "detail": "The start date (2026-01-01) must not be after the end date (2025-12-31). Please review the contract item." } ``` The `detail` field is human-readable and actually useful — it tells you what went wrong. Parse the `type` field for programmatic error handling, and surface `detail` in your logs. ```javascript async function handleRilletResponse(response) { if (!response.ok) { const error = await response.json().catch(() => null); const detail = error?.detail ?? `HTTP ${response.status}`; throw new Error(`Rillet API error: ${detail}`); } return response.json(); } ``` --- ## Monetary Values Financial amounts are returned as strings, not floats — which is the correct approach for avoiding floating point precision issues. Each amount comes with a currency code: ```json { "amount": "100.99", "currency": "USD" } ``` The `currency` field is always an ISO-4217 three-letter code. Note that Rillet allows more decimal places than the ISO standard for certain fields like unit prices, so don't assume two decimal places — parse flexibly. Use a decimal library (like `decimal.js` or Python's `decimal` module) rather than JavaScript's native `Number` when doing arithmetic on these values. --- ## Webhooks Rather than polling for changes, you can subscribe to real-time events via webhooks. Rillet supports events across entities like `Invoice`, `Customer`, `Payment`, and `Credit Memo`, with event types like `CREATED` and `UPDATED`. ### Setup Go to Organization Settings > Webhooks in the Rillet dashboard: 1. Give it a name. 2. Provide a publicly accessible HTTPS URL. 3. Select the events you want to receive. 4. Enable the webhook. An organization can have up to 5 webhooks. The recommendation from Rillet's docs is to use a single endpoint and handle routing in your application. ### Incoming Request Structure Every webhook request is a POST with a JSON body and these headers: | Header | Description | |---|---| | `X-Rillet-Signature` | HMAC-SHA256 signature (Base64-encoded) | | `X-Rillet-Timestamp` | ISO 8601 timestamp | | `X-Rillet-Id` | Unique UUID for this delivery | | `X-Rillet-Entity` | Entity type (e.g., `INVOICE`) | | `X-Rillet-Event` | Event type (e.g., `CREATED`) | Your endpoint must respond with a `2xx` status within 30 seconds. Rillet retries failed deliveries in groups of 3 with exponential backoff, up to 5 groups before marking the webhook as failed. Use `X-Rillet-Id` for idempotency on your side — deduplicate incoming events by this ID. ### Verifying Signatures Always verify the signature. The signature is an HMAC-SHA256 hash of a concatenated payload, Base64-encoded. The signed payload format is: ``` {timestamp}.{id}.{entity}.{event}.{raw_body} ``` Here's a TypeScript implementation: ```typescript import * as crypto from 'crypto'; import { Buffer } from 'node:buffer'; export function verifyRilletWebhook( headers: Record