Go-live checklist
Before the first real invoice goes out: eight points an integration should cover. Each one links to the page with the details.
The eight points
- 1
Switch to the live key
Replace
sk_test_…withsk_live_…. Host, paths and responses stay the same; the prefix does not change how the API behaves. Both keys count against the same monthly quota. - 2
Read the key from the environment only
The key belongs in an environment variable or a secret manager, never in code, the query string or the request body. It is sent in the
Authorizationheader as a bearer token. - 3
Tell the two kinds of 429 apart
If the response carries a
quotaobject (QUOTA_EXCEEDED), the monthly quota is used up: do not retry. Without aquotaobject the rate limit applies: retry with exponential backoff, since noRetry-Afterheader is sent. - 4
Honor
Retry-Afteron 503Responses such as
SERVER_BUSY(5 seconds) orEMBED_UNAVAILABLE(60 seconds) state in theRetry-Afterheader how long to wait. Nothing is billed. - 5
Do not retry 4xx
A 400 or 422 is fixed in the data, not by a second attempt. On 422,
errors[]names the rule that failed. - 6
Detect duplicate invoices yourself
The API has no
Idempotency-Keyheader. Every generate response carriespayloadHash, a SHA-256 over the canonical JSON of your invoice data: same data, same value. Store it with the invoice and check for it before calling again.hashdoes not work for this: it changes on every call. - 7
Log the request id
Every response carries
x-request-id. Log it with every error; support uses it to find your call. - 8
Check your plan against your volume
The pricing calculator shows which plan fits your monthly invoice volume and what it costs.
Putting it together: one call with retry
Retries only what a retry can fix, waits as long as Retry-After says, and includes the request id in the error.
1async function callInvoiceApi(url, body, attempts = 4) {2 for (let i = 0; i < attempts; i++) {3 const res = await fetch(url, {4 method: "POST",5 headers: {6 Authorization: `Bearer ${process.env.INVOICE_API_KEY}`,7 "Content-Type": "application/json",8 },9 body: JSON.stringify(body),10 });11 if (res.ok) return res.json();12 13 const requestId = res.headers.get("x-request-id");14 const err = await res.json().catch(() => ({}));15 16 // 429 with a quota object = monthly quota used up: retrying cannot help.17 // 429 without it = rate limit. 5xx = try again. 4xx = fix the data.18 const retryable = (res.status === 429 && !err.quota) || res.status >= 500;19 if (!retryable || i === attempts - 1) {20 throw new Error(`${res.status} ${err.error ?? ""} (request ${requestId})`);21 }22 23 // 503 SERVER_BUSY / EMBED_UNAVAILABLE say how long to wait; otherwise back off.24 const retryAfter = Number(res.headers.get("retry-after"));25 const waitMs = retryAfter > 0 ? retryAfter * 1000 : 2 ** i * 1000;26 await new Promise((resolve) => setTimeout(resolve, waitMs));27 }28}Not there yet?
The quickstart gets you to your first invoice in five minutes with a test key.