API changelog
Every change to the Kintsugi API and every release, newest first. Breaking changes carry the Breaking change mark. Tenanted releases are dated, and you choose one with the Api-Version header.
Changes
2026-10-07
Breaking changesLimits billing changes to an organization's Owners, Admins and own API keyBillingBreaking change
POST /billing/checkout, POST /billing/portal-session, PATCH /billing/plan and PATCH /billing now accept only the organization's own Owners and Admins and the organization's own API key. Any other caller that can reach the organization, including a Member, a partner user and a portfolio or reseller key, answers 403 with code forbidden; these callers were accepted before. A user acting through a portfolio is refused the same way. An organization you cannot access still answers 404, and the billing read endpoints are unchanged.
- Start checkouts, portal sessions and plan changes from the organization's own Owner or Admin session, or with its organization API key.
- Handle
403with codeforbiddenfrom these four endpoints.
2026-10-05
Breaking changesLimits pausing a filing to the window before its due dateFilingsBreaking change
Pausing a filing now follows the same window as the Kintsugi app. After the 15th of the month a filing is due, it can no longer be paused with any pauseIntent, including assistance and skip, which were accepted before. A review pause also needs a pausedUntilDate from today through the 15th of the due month; a date in the past, accepted before, is now refused. A date that is still today anywhere in the world counts as today. POST /filings/{filing_id}/pause answers a refused pause with 400 and code invalid_request, and POST /filings/bulk/pause still answers 200, listing each refused filing in failed.
- Pause a filing no later than the 15th of the month it is due.
- For a
reviewpause, send apausedUntilDateno earlier than today and no later than the 15th of the due month. - After a bulk pause, check
failedfor filings that were not paused.
Returns already_connected when an activated store is already connectedConnectionsBreaking change
When POST /connections/apideck/{conn_id}/activate finds that the shopId belongs to a connection your organization already has, it reactivates that existing connection, removes the one being activated, and answers 400 as before. The error code is now already_connected instead of invalid_request, with the message This store is already connected. The existing connection was reactivated. The store is connected and nothing more is needed. already_connected is a new value of the code field in error responses.
- Treat
400with codealready_connectedas success, and list your connections again to find the one that remains. - Stop relying on
invalid_requestto detect an already connected store. - If your client accepts only known
codevalues, addalready_connected.
2026-10-02
Breaking changesApplies one access rule to every address changeAddressesBreaking change
The five endpoints that change addresses now accept the same callers: any member of the organization, a portfolio Owner or Admin who manages it, or an API key for the organization. A portfolio Member now receives 403 with code forbidden from POST /addresses/approve, which accepted them before. The other four, which accepted only the organization's Owner among its own users, now also accept its Admins and Members, who received 403 before. An organization you cannot access still answers 404, and API keys work on all five as before.
- Have a portfolio Owner or Admin approve addresses, or approve with the organization's API key.
- Handle
403with codeforbiddenfromPOST /addresses/approve. - If your app hid these address actions from organization Admins and Members, you can now offer them.
Requires a non-empty userId for imports started with an API keyImportsBreaking change
On POST /imports/initiate and POST /imports/upload-urls, whether you send userId now depends on your credential. With an API key it is required and must not be blank: a missing userId answers 400 with code invalid_request instead of 422, and an empty or whitespace-only value, accepted before, is now refused the same way. With a signed-in user session, userId is optional and ignored, and the import is recorded under that user.
- With an API key, always send a non-empty
userIdthat names the person or process uploading. - Expect
400with codeinvalid_request, not422, whenuserIdis missing.
2026-09-22
Breaking changesRequires a request body to deregister a registrationRegistrationsBreaking change
Deregistering now records when and why the registration closes. POST /v1/registrations/{registration_id}/deregister requires a JSON body with closure_date, reason and final_return_acknowledged, so a request with no body, which succeeded before, is now rejected with 422. The 2026-07-21 release changed the same way, with the fields closureDate, reason and finalReturnAcknowledged.
- Send
closure_date: the date the registration closes with the jurisdiction, as YYYY-MM-DD. Past and future dates are accepted. - Send
reason:FULL_BUSINESS_CLOSUREorCLOSING_NEXUS_IN_STATE. - Send
final_return_acknowledged: true, confirming that a final return is still owed.
Returns the existing credit note when a credit note is created twiceTransactionsBreaking change
Creating a credit note with an external_id already used for a credit note on the same original transaction used to fail with 409 Conflict. It now succeeds and returns the stored credit note unchanged. On v1 that response is 200, and creating a new credit note now answers 201 instead of 200. On the 2026-07-21 release the stored credit note comes back with 202, the same as a new one. The same external_id against a different original transaction still returns 409.
- On v1, treat both
201and200as success. - Stop relying on
409to detect an existing credit note. - To change an existing credit note, update it:
PUT /v1/transactions/{original_transaction_id}/credit_notes/{credit_note_id}on v1, orPATCH /transactions/{transaction_id}on the 2026-07-21 release.
2026-09-21
Breaking changesReturns the existing record when a physical nexus is created twiceNexusBreaking change
Creating a physical nexus for an organization, country, state and category that already has one used to fail with 409 Conflict. It now succeeds and returns the stored record unchanged: the dates and address in the request are not applied. On v1 and the Partner API the response is 200, the same as for a new record. On the 2026-07-21 release it is 200 for an existing record and 201 for a new one.
- Stop relying on
409to detect an existing record. - To change an existing record's dates or address, update it:
PUT /v1/nexus/physical_nexus/{physical_nexus_id}on v1, orPATCH /physical-nexus/{physical_nexus_id}on the 2026-07-21 release.
2026-09-17
Breaking changesReturns the existing product when a product is created twiceProductsBreaking change
Creating a product with an external_id and source that already exist used to fail with 409 Conflict. It now succeeds with 200 and returns the stored product unchanged: the fields in the request are not applied. A product you deleted is restored instead, with status PENDING, and is not used in tax calculation until it is approved again. Creating a new product now answers 201 instead of 200.
- Treat both
201and200as success. - Stop relying on
409to detect an existing product. - To change an existing product, update it with
PUT /v1/products/{product_id}.
Releases
2026-10-06
LatestTenanted APIConsistent country codes, empty values and status casing
2026-10-06 makes the Tenanted API more predictable to build on. Every country code is now countryCode, a value that is not set is null rather than an empty string, and status and type values are uppercase throughout. Integrations pinned to 2026-07-21 are unaffected until you choose to move, and the table below lists everything that changes when you do.
Compared with 2026-07-21
| Change | Applies to | 2026-07-21 | 2026-10-06 |
|---|---|---|---|
| Country code field | Addresses, customers, transactions, tax estimates and billing usage, in requests and responses | country | countryCode. Query parameters named country keep their name. |
| State on nexus exposure events | GET /nexus/{nexus_id}/exposure-history | state | stateCode, always present |
| State of a country-level filing | stateCode and stateName on every filing | Empty string | null |
| Organization state and country when not set | state and countryCode on organizations, and state in organization lists | Empty string, always present | null, and may be omitted |
| Exemption with no match on a certificate upload | exemptionId on missing-certificate upload results | Empty string | null |
| Missing-certificate status values | Upload, upload status and public confirm-upload results | Lowercase: processing, satisfied, rejected | Uppercase: PROCESSING, SATISFIED, REJECTED, plus PENDING on the public landing page |
| Exemption type when approving certificate imports | POST /certificate-imports/{certificate_import_id}/approve and POST /certificate-imports/bulk-approve | Any text, defaulting to customer | CUSTOMER, WHOLESALE, TRANSACTION or REVERSE_CHARGE, defaulting to CUSTOMER. Any other value returns 422. |
| Security questions in a credential reveal | fields on POST /registrations/{registration_id}/credentials/reveal | security_questions | securityQuestions |
| Line item with no product name | productName on transaction lines | null | Empty string |
| Help article label on back-filing options | GET /filings/backFilingRequest/options | Text or null | Always text |
| Job ID on bulk filing results | POST /filings/bulk/approve and POST /filings/bulk/pause | jobId, always null | Removed |
Moving an integration from 2026-07-21
- Send
Api-Version: 2026-10-06with each request you move. Requests without it keep running against 2026-07-21. - Rename
countrytocountryCodewherever you send or read an address, customer, transaction or tax estimate. - Treat
nullas "not set" for filing and organization locations and for an unmatchedexemptionId, and an emptyproductNameas "no product name". - Compare missing-certificate statuses in uppercase, and send
exemptionTypein uppercase when you approve certificate imports. - Read
stateCodeon nexus exposure events, requestsecurityQuestionswhen you reveal registration credentials, and stop readingjobIdon bulk filing results.
2026-07-21
Tenanted APIIntroducing the Tenanted API
A fresh foundation for building on Kintsugi. The Tenanted API is versioned by date, and you choose the release you build against. It speaks one consistent dialect from end to end: clean paths, camelCase fields, exact decimal amounts, and a single error format. And it is built for scale, with one credential reaching every organization it owns.
Compared with v1
| Topic | v1 | 2026-07-21 |
|---|---|---|
| Versioning | The /v1 path prefix | The Api-Version header, dated YYYY-MM-DD. Optional; without it, a request runs against the launch release |
| Paths | /v1/transactions | /transactions |
| Authentication | x-api-key | Api-Key. Endpoints that manage people accept a signed-in session token instead |
| Choosing an organization | x-organization-id, required on almost every request | One credential reaches every organization it owns. Narrow a request with Organization-Id, Connection-Id, or Entity-Id |
| Field names | snake_case | camelCase |
| Money and rates | Decimal strings out, numbers or strings in | Decimal strings at a fixed scale: amounts to 2 places, rates to 9 |
| Timestamps | No single documented format | RFC 3339 in UTC, always ending in Z |
| Errors | Shapes vary by endpoint | One envelope everywhere: code, message, requestId, and errors |
| Portfolio | Not available | Portfolio and Portfolio Reseller endpoints, for partner accounts |
Moving an integration from v1
- Send
Api-Versionwith every request, pinned to the release you build against (the newest is2026-10-06), so your integration stays on it until you decide to move. - Drop
/v1from your paths. - Send your key as
Api-Key, and replacex-organization-idwithOrganization-Idwherever you target one organization. - Rename fields to camelCase, and read every amount as a decimal string, never a float.
- Branch on the error
code, not the message or the HTTP status, and quoterequestIdwhen you contact support.
Creating a transaction answers 202 Accepted straight away, and tax is calculated just after. Check processingStatus before you read the tax totals.
v1
LegacyThe original Kintsugi API, versioned in the path. It is documented exactly as before, so existing integrations can keep running with confidence. New integrations should start on the newest Tenanted API release.