How to Integrate with the Rillet API in 2026

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.

GJGJ

GJ

12 min read
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.


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:

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.

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:

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:

{
  "data": [...],
  "pagination": {
    "next_cursor": "VdW1ptsZbOB4E1fq"
  }
}

To fetch the next page, pass the cursor as a query parameter:

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:

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:

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:

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:

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:

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:

{
  "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.

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:

{
  "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:

HeaderDescription
X-Rillet-SignatureHMAC-SHA256 signature (Base64-encoded)
X-Rillet-TimestampISO 8601 timestamp
X-Rillet-IdUnique UUID for this delivery
X-Rillet-EntityEntity type (e.g., INVOICE)
X-Rillet-EventEvent 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:

import * as crypto from 'crypto';
import { Buffer } from 'node:buffer';

export function verifyRilletWebhook(
  headers: Record<string, string>,
  rawBody: string,
  token: string
): void {
  const signatures = (headers['X-Rillet-Signature'] ?? '')
    .split(',')
    .map(s => s.trim())
    .filter(Boolean);

  const timestamp = headers['X-Rillet-Timestamp'];
  const id = headers['X-Rillet-Id'];
  const entity = headers['X-Rillet-Entity'];
  const event = headers['X-Rillet-Event'];

  if (!signatures.length || signatures.length > 10 || !timestamp || !id || !entity || !event) {
    throw new Error('Missing or invalid Rillet webhook headers');
  }

  const signedPayload = `${timestamp}.${id}.${entity}.${event}.${rawBody}`;
  const tokenBytes = Buffer.from(token, 'base64');

  const verified = signatures.some(sig => {
    const expected = crypto
      .createHmac('sha256', tokenBytes)
      .update(signedPayload)
      .digest('base64');

    const receivedBuf = Buffer.from(sig, 'base64');
    const expectedBuf = Buffer.from(expected, 'base64');

    return (
      receivedBuf.length === expectedBuf.length &&
      crypto.timingSafeEqual(receivedBuf, expectedBuf)
    );
  });

  if (!verified) throw new Error('Invalid webhook signature');
}

And the Python equivalent:

import hashlib
import hmac
import base64

def verify_rillet_webhook(headers, raw_body, token):
    signatures = [s.strip() for s in headers.get('X-Rillet-Signature', '').split(',') if s.strip()]
    timestamp = headers.get('X-Rillet-Timestamp')
    event_id = headers.get('X-Rillet-Id')
    entity = headers.get('X-Rillet-Entity')
    event = headers.get('X-Rillet-Event')

    if not signatures or len(signatures) > 10 or not all([timestamp, event_id, entity, event]):
        raise ValueError('Missing or invalid Rillet webhook headers')

    signed_payload = f"{timestamp}.{event_id}.{entity}.{event}.{raw_body}"
    token_bytes = base64.b64decode(token)

    for sig in signatures:
        expected = hmac.new(token_bytes, signed_payload.encode('utf-8'), hashlib.sha256).digest()
        received = base64.b64decode(sig)
        if hmac.compare_digest(received, expected):
            print('Signature verified.')
            return

    raise ValueError('Invalid webhook signature')

One edge case worth knowing: the signature header can contain up to 10 comma-separated signatures to support token rotation without downtime. Your code should verify any of them, not just the first.


Using the Rillet MCP Server

This is the part that's new in 2026 and genuinely useful if you're using AI coding agents to build or maintain your integration.

Rillet ships a remote MCP server at https://docs.api.rillet.com/mcp. You can connect AI tools like Claude Code, Cursor, Windsurf, or Claude Desktop to it, and they'll have direct access to your Rillet account data and documentation. This means your AI assistant can look up your actual customers, check for overdue invoices, or generate integration code that reflects your real chart of accounts.

Setting Up with Claude Code

# Sandbox
claude mcp add --transport http rillet-mcp-sandbox \
  https://docs.api.rillet.com/mcp \
  --header "Authorization: Bearer YOUR_SANDBOX_KEY"

# Production
claude mcp add --transport http rillet-mcp-prod \
  https://docs.api.rillet.com/mcp \
  --header "Authorization: Bearer YOUR_PROD_KEY"

# Verify
claude mcp list

When prompting your AI agent, specify which environment to use: "Use the Rillet MCP in sandbox." From there you can ask questions like "What customers do I have?" or "Do I have any overdue invoices?" and get answers grounded in your actual data.

This is particularly useful during development β€” instead of manually reading through API docs and constructing test requests by hand, you can have an AI agent explore the API surface against your sandbox data while you're building.


Pulling Financial Reports

Rillet has been adding financial report endpoints through early 2026. The current set includes:

# Balance sheet
GET /reports/balance-sheet

# Trial balance
GET /reports/trial-balance

# Income statement
GET /reports/income-statement

# Cash flow statement
GET /reports/cash-flow-statement

All four support subsidiary_id and breakdown_by parameters for multi-entity setups. The trial balance returns beginning balance, debits, credits, and ending balance per account. The income statement and cash flow statement both accept a date range. There's also a reporting journal entries endpoint (GET /reports/journal-entries) that returns amounts converted to the subsidiary's reporting currency β€” useful for multi-currency consolidations.


A Minimal Integration Example

To tie this all together, here's a minimal Node.js integration that fetches invoices from Rillet:

import fetch from 'node-fetch';

const BASE_URL = 'https://api.rillet.com';
const API_KEY = process.env.RILLET_API_KEY;
const headers = {
  'Authorization': `Bearer ${API_KEY}`,
  'X-Rillet-API-Version': '3',
};

async function syncInvoices() {
  const invoices = [];
  let cursor = null;

  do {
    const url = new URL(`${BASE_URL}/invoices`);
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);

    const response = await fetch(url.toString(), { headers });

    if (!response.ok) {
      const error = await response.json().catch(() => ({}));
      throw new Error(`Rillet error: ${error.detail ?? response.status}`);
    }

    const data = await response.json();
    invoices.push(...data.data);
    cursor = data.pagination?.next_cursor ?? null;
  } while (cursor);

  return invoices;
}

// Usage
const invoices = await syncInvoices();
console.log(`Fetched ${invoices.length} invoices`);

Summary

The Rillet API is well-designed for a relatively new platform. A few things stand out: the OpenAPI-first approach means you can generate typed clients, updated_at timestamps across most entities make incremental sync viable (check entity-by-entity which ones support updated.gt filtering), and the MCP server is a genuinely useful addition for teams using AI coding tools.

The main things to get right from the start are version pinning, idempotency keys on write operations, webhook signature verification, and using a decimal library for monetary arithmetic. Get those four right and the rest is just building out the specific data flows your product needs.

For the full API reference, see docs.api.rillet.com and the changelog for what's new.

Ready to get started?

Scale your integration strategy and deliver the integrations your customers need in record time.

Ready to get started?
Talk to an expert

Trusted by fast-moving product & engineering teams

JobNimbus
Blue Zinc
Drata
Octa
Nmbrs
Apideck Blog

Insights, guides, and updates from Apideck

Discover company news, API insights, and expert blog posts. Explore practical integration guides and tech articles to make the most of Apideck's platform.