SET Close
SET API (Platforms) and SET Merchant API · Live since 2026-11-11
The problem
At month end, finance has to show that every payment moved as recorded. Several records describe each payment: the provider's record, the bank's line, the chain's transfer, and the entry in your books. When one is missing or two disagree, someone finds it by hand. Then the month has to be signed off and kept as it was.
The answer
SET Close keeps each payment's records side by side and matches them. It names every gap, and locks the month after two sign-offs. It moves no money and signs nothing: it reads.
- Records come in by themselves. For money that moves through SET, Close reads SET's own ledger every 10 minutes: payouts, deposits, Cash-Out payments, Direct Deposit payee cash-outs, wallet withdrawals, and your journal entries.
- You add your bank's file. Upload the bank statement (or another provider's export) as a CSV. Each line is matched to its payment.
- Each payment gets one match. The match is
matchedor anexceptionthat names the problem. A payment that is not final yet ispending. - You resolve the exceptions, then two keys sign off. One key prepares the month, and a different key with the
approvescope locks it. The lock records a hash that you can verify later.
Gross, fee and net
SET's fee comes out of the payment, so the chain and the bank see different amounts. Close compares each pair of records on the amount both sides actually saw:
| Payment | The chain shows | The bank shows |
|---|---|---|
| A payment into a Cash-Out Address | the gross (100.00 USDC) | the net in its own currency (98.50 USD) |
| A bank deposit into a virtual account | the stablecoin delivered | the gross received |
| A payout to a bank | n/a | the net in the bank's currency |
| A payout to a wallet | the net delivered | n/a |
Every record carries gross, fee and net. A stablecoin is compared with the currency it tracks, to the cent.
Matching
The first rule that finds exactly one payment wins. Two or more is ambiguous, never a guess.
linked: the records came from the same SET payment.hash: the same transaction hash on the same chain.reference: the bank line names the payment's bank reference (an ACH trace number, a wire IMAD, an end-to-end id) or your invoice number, and the amounts agree.amount_date: the same amount, currency and direction, within 3 calendar days, and no other open payment fits.
Exceptions
| Reason | What it means |
|---|---|
missing_rail | No bank record yet: the bank's reference has not been reported, or no bank line matched |
missing_provider | A bank or provider line that belongs to no payment SET knows |
missing_chain | No on-chain transfer for a payment that needs one |
missing_book | A payment of a paid invoice with no payout journal entry |
amount_mismatch | Two records disagree, with the variance |
ambiguous | More than one payment fits a line |
chain_unconfirmed | An uploaded transaction hash that SET's own chain read has not confirmed |
duplicate | Two identical lines in a file without row ids |
prior_period | Something changed, or arrived, for a month already locked; it is carried into the open month |
Resolve each exception in one of three ways. link it to a record: the payment must then agree, by the same rules. ignore it with a note. Or leave_open with a note: it stays an exception, but no longer blocks the month.
POST /v1/close/exceptions/cm_…/resolve
{ "action": "leave_open", "note": "Trace number requested from the bank." }Months, sign-off and the lock
Months are calendar months in UTC. Close starts with the month you first use it; set startPeriod to start earlier. Payments from before then are ignored as before_start.
- Money out belongs to the month it left, and money in to the month it was credited.
- Sent on the 31st, paid on the 2nd: the payment stays in the earlier month as an in-transit reconciling item, not an exception.
- A return is its own entry,
reversalOfthe payment, in the month it was reported. The pair nets to zero.
POST /v1/close/periods/2026-10/prepare (a key with the write scope)
POST /v1/close/periods/2026-10/approve (a different key, with the approve scope)
GET /v1/close/periods/2026-10/verify- Prepare works only after the month ends, with no exception left without a note.
- Approve is refused (
same_approver) to the preparing key, and to any key rotated from it or to it. It needs the month before locked first. - The lock records
snapshotHash: the SHA-256 of the month's matches followed by the previous month's hash.verifyrecomputes it. A locked month never changes; a later change appears in the open month asprior_period. - A new exception after prepare, before approve, opens the month again.
Both keys belong to your business, so this is two-key approval, not an independent attestation.
The package
GET /v1/close/periods/2026-10/package?format=csv
GET /v1/close/periods/2026-10/package?customer=cus_…Every match in the month is included, with:
- its record ids and what each pair was compared on;
- the signed amount, the rule, the reason and your note;
- the reconciling items;
- the bank lines' references and counterparties;
- the hash, once the month is locked.
customer= cuts it to one of your customers. Cells a spreadsheet would run as a formula are escaped.
Uploading a bank file
POST /v1/close/sources { "kind": "bank_csv", "label": "Operating account" }
POST /v1/close/sources/src_…/imports (the CSV as the body, text/csv)Columns: id, date, amount, currency, direction, reference, counterparty, tx_hash, chain. The first five are required. A bank file with no row ids can use ?idStrategy=row_hash.
The format:
- one header row;
- UTF-8, with or without a byte-order mark;
- commas between columns, and a dot for decimals;
- at most 1,000 rows and 1 MB.
What Close refuses:
- A decimal comma, more decimals than the currency has, or a signed amount: that row is refused, with its number, and the rest of the file is accepted.
- A semicolon-separated file: the whole file is refused. Close does not guess.
Files are kept only as needed. The same file uploaded twice adds nothing. The file itself is not kept, only its SHA-256 and the counts.
Your books
If you use the SET Merchant API with QuickBooks Online or Xero connected, SET posts its journal entries when Close says so. With postOn: "approve" (the default) an entry posts once its month is locked. With "match" it posts once its payment matches.
PUT /v1/close/settings { "postOn": "approve" }Each entry is posted once, even if an answer is lost and the post is retried. When your ledger refuses one, you get close.post.failed and the match stands. The package lists every entry not yet posted.
What is kept, and what is not
- References and counterparties from your files are never stored as text. Each is kept as a keyed hash, which is how lines are matched, and a sealed copy that only the package opens.
- A deletion data request for a customer drops the sealed copies; ids, amounts, dates and hashes stay.
- An uploaded line is
asserted. It is never proof on its own.
Limits
- Daily writes: Close has a daily allowance for each business, so reporting can never slow a payment. When it is reached, a file is refused whole (
429 close_budget_reached) and the ledger reading resumes the next day (UTC). - What it reconciles: money that moves through SET, plus the files you upload. Close does not watch other accounts or chains for you.
- What "matched" means: the records SET holds agree. It is not an audit opinion, a reserve attestation, or a statement that any payment complies with any rule.
Calls
| Call | Scope | Does |
|---|---|---|
GET, PUT /v1/close/settings | read, write | The first month, postOn, today's writes |
GET, POST /v1/close/sources | read, write | Your sources: SET's ledger and the files you label |
POST /v1/close/sources/{source}/imports | write | Upload a CSV |
GET /v1/close/imports | read | Your uploads, with their counts |
GET /v1/close/records | read | Records; kind, source, period, customer, unmatched=true |
GET /v1/close/matches | read | Matches; period, state, rule, reason, customer, inTransit |
GET /v1/close/exceptions | read | Exceptions; reason, period, customer |
POST /v1/close/exceptions/{id}/resolve | write | link, ignore or leave_open |
GET /v1/close/periods | read | Every month from the first, with counts |
GET /v1/close/periods/{period} | read | One month, its reconciling items and hashes |
POST /v1/close/periods/{period}/prepare | write | Open to prepared |
POST /v1/close/periods/{period}/approve | approve | Prepared to locked |
GET /v1/close/periods/{period}/package | read | JSON or CSV |
GET /v1/close/periods/{period}/verify | read | Recompute the lock's hash |
GET /v1/close/issuers | read | Each stablecoin SET knows, its contract and issuer |
Events: close.exception.opened, close.exception.resolved, close.period.prepared, close.period.locked, close.post.failed. They carry ids, states, amounts and hashes only.
Capabilities: GET /v1/capabilities reports features.close and features.accountingConnectors.