SETSimple Easy Transactions
SET DocsAPI reference and complianceUpdated 2026-11-10
Browse: SET API (Platforms)

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.

  1. 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.
  2. 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.
  3. Each payment gets one match. The match is matched or an exception that names the problem. A payment that is not final yet is pending.
  4. You resolve the exceptions, then two keys sign off. One key prepares the month, and a different key with the approve scope 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:

PaymentThe chain showsThe bank shows
A payment into a Cash-Out Addressthe gross (100.00 USDC)the net in its own currency (98.50 USD)
A bank deposit into a virtual accountthe stablecoin deliveredthe gross received
A payout to a bankn/athe net in the bank's currency
A payout to a walletthe net deliveredn/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.

  1. linked: the records came from the same SET payment.
  2. hash: the same transaction hash on the same chain.
  3. 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.
  4. amount_date: the same amount, currency and direction, within 3 calendar days, and no other open payment fits.

Exceptions

ReasonWhat it means
missing_railNo bank record yet: the bank's reference has not been reported, or no bank line matched
missing_providerA bank or provider line that belongs to no payment SET knows
missing_chainNo on-chain transfer for a payment that needs one
missing_bookA payment of a paid invoice with no payout journal entry
amount_mismatchTwo records disagree, with the variance
ambiguousMore than one payment fits a line
chain_unconfirmedAn uploaded transaction hash that SET's own chain read has not confirmed
duplicateTwo identical lines in a file without row ids
prior_periodSomething 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, reversalOf the 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. verify recomputes it. A locked month never changes; a later change appears in the open month as prior_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

CallScopeDoes
GET, PUT /v1/close/settingsread, writeThe first month, postOn, today's writes
GET, POST /v1/close/sourcesread, writeYour sources: SET's ledger and the files you label
POST /v1/close/sources/{source}/importswriteUpload a CSV
GET /v1/close/importsreadYour uploads, with their counts
GET /v1/close/recordsreadRecords; kind, source, period, customer, unmatched=true
GET /v1/close/matchesreadMatches; period, state, rule, reason, customer, inTransit
GET /v1/close/exceptionsreadExceptions; reason, period, customer
POST /v1/close/exceptions/{id}/resolvewritelink, ignore or leave_open
GET /v1/close/periodsreadEvery month from the first, with counts
GET /v1/close/periods/{period}readOne month, its reconciling items and hashes
POST /v1/close/periods/{period}/preparewriteOpen to prepared
POST /v1/close/periods/{period}/approveapprovePrepared to locked
GET /v1/close/periods/{period}/packagereadJSON or CSV
GET /v1/close/periods/{period}/verifyreadRecompute the lock's hash
GET /v1/close/issuersreadEach 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.