Changelog
All notable API changes documented here.
2026-10-27 — Business time zone and monthly totals
PATCH /api/v1/merchants/meacceptstimezone, an IANA name such asEurope/Madrid.- The server stores the canonical spelling:
europe/madridis saved asEurope/Madrid. - An empty string clears the time zone.
- Fixed offsets (
+01:00,Etc/GMT+3) and unknown names return400. GET /api/v1/merchants/mereturnstimezone, ornullwhen it is not set.
- The server stores the canonical spelling:
- You can also set it in the dashboard under Settings → Time zone.
- Mobile API (
/mobile/v1, contract1.0.0-draft.21):GET /mereturns each business’stimezone.GET …/homereturnsmonthwhen the business has a time zone. The period starts on the 1st of the month at 00:00 in that time zone.- Each currency in
month.totalshas three numbers:received— payments from customers;otherIn— other incoming transfers;sent— outgoing transfers.
- Only confirmed transfers count. Conversions are not included.
- Without a time zone,
monthstaysnull.
2026-10-26 — Mobile API: push devices
Applies to the mobile API (/mobile/v1, contract 1.0.0-draft.20).
POST /mobile/v1/devicesregisters the current session’s device for push.- iOS sends an APNs token. An installed PWA sends a Web Push subscription with an https endpoint.
- The platform must match the session’s platform.
- A device has one registration. Registering again replaces the token and turns push back on.
- Returns
201with the samedeviceIdthat the client sent at sign-in. - Errors:
422 device_invalid— the platform does not match, the token or the subscription keys are missing, or the endpoint is not https or points to an internal address.
DELETE /mobile/v1/devices/{deviceId}turns push off on any device you signed in from, for example a lost phone. Repeating the call also returns204. Another person’s device or an unknown device returns404.- Sending push is not part of this change: devices are only registered for now.
2026-10-26 — Mobile API: saved recipients
Applies to the mobile API (/mobile/v1, contract 1.0.0-draft.19).
GET /mobile/v1/businesses/{businessId}/recipientslists saved recipients, recently used first. Search withqby name or address.POST …/recipientssaves a recipient. These are the same recipients as in the dashboard.- Saving the same network and address again renames it; no second entry is created.
- The address is stored in canonical form (EIP-55 on EVM).
- Errors:
422 address_invalid,422 name_required,409 idempotency_conflict.
- Not yet:
archivedis alwaysfalse;- the
similar_to_savedwarning is not returned.
2026-10-26 — Mobile API: payment requests (Get paid)
Applies to the mobile API (/mobile/v1, contract 1.0.0-draft.18).
POST /mobile/v1/businesses/{businessId}/payment-requestscreates a payment request.- The asset of
amountsets the currency: USDC or USDT means USD, EURC means EUR. - Your customer can pay with any token of that currency on any network where you receive. The request also appears in the dashboard.
- Open
payUrlto pay. - Repeating the call with the same
Idempotency-Keyreturns the same request. - Errors:
422 asset_unsupported— an asset other than USDC, USDT or EURC;422 amount_below_minimum;422 expiry_in_past;422 receiving_not_set— you do not receive the currency on any network;409 idempotency_conflict.
- The asset of
GET …/payment-requestslists requests newest first, with a cursor and astatusfilter:open,partly_paid,paid,overpaid,accepted_as_paid,expired,cancelled.GET …/payment-requests/{requestId}returns one request withpaid, the amount actually received.POST …/payment-requests/{requestId}/accept-as-paidcloses a partly paid request as paid. It is idempotent by key and answers409 invalid_statewhen the request is notpartly_paid.
2026-10-26 — Mobile API: Activity feed
Applies to the mobile API (/mobile/v1, contract 1.0.0-draft.17).
GET /mobile/v1/businesses/{businessId}/activityis live. One feed of money in and out:- payments for requests and checkout (
Payment for <reference>, the amount actually received); - Send and Convert from the app;
- payouts and swaps from the dashboard;
- transfers seen only in the wallet, such as Add money.
- payments for requests and checkout (
- A transfer that is both a payment and a wallet movement appears once.
- Newest first.
cursor/nextCursorstay stable when new entries arrive between pages. - Search with
qby name, address, request reference or transaction hash. Filter bydirection,status,currency,walletId,from/to. GET …/activity/{itemId}returns one entry.GET …/home:recentnow holds the latest five entries of the same feed. It is[]when there are none.- Not yet: suspicious-incoming detection.
suspiciousis alwaysfalse,hiddenCountis always0, and theneeds_reviewstatus does not occur.POST …/needs-you/{itemId}/resolveanswers409 invalid_statefor items that clear themselves and404otherwise.
2026-10-26 — Mobile API: an operation without an outcome becomes unknown after 3 minutes
Applies to the mobile API (/mobile/v1, contract 1.0.0-draft.16).
GET /mobile/v1/businesses/{businessId}/operations/{operationId}: poll no longer than 3 minutes after sending (2 → 4 → 8 → 15 s). If no outcome is observed by then, the server moves the operationsubmitted → unknownitself. Show “outcome unknown” withPOST …/check-statusinstead of an endless spinner.- Nothing is sent again from
unknown. The server keeps looking for the outcome;check-statuspicks it up. The Needs you itemoperation_unknownstays until the operation isconfirmedorfailed.
2026-10-26 — Breaking: wallet key actions use interactive Sign sessions
Eight authenticated POST endpoints under /api/v1/wallets are retired:
/embedded/token, /embedded/encryption-session, /embedded/passkeys,
/embedded/attempt, /embedded, /embedded/:id/rekey/release,
/embedded/:id/rekey/complete and /embedded/:id/exported.
They return 410 SIGN_WINDOW_REQUIRED, a migration link and no key material
or lifecycle database changes.
Use an owner’s merchant JWT to open the appropriate task at
POST /api/v1/signing-sessions, then open its returned /w#... URL in a
separate Sign window. Creation, import, backup/export and passkey reissue each
have an explicit session kind. There is no generic vendor-token or unattended
server-side replacement. Read the subject-bound outcome from the server;
window messages and a submitted signature are not proof of a completed transfer.
See Wallet Signing migration for all eight mappings, a tested request example, authentication and popup/outcome handling. Payment API keys and read-only wallet metadata are unchanged.
2026-10-24 — Tron bridges accept both signatures before sending
Fresh Tron bridge drafts retain both the exact USDT approval and the bridge
transaction. POST /exchange/bridge/submit requires
signatures: [approveSignature, routeSignature]; a lone signature or client
txHash is refused for Tron. The response is initially source_queued, not
an assertion that funds have moved. Embedded wallets sign the pair in inzi sign.
The server confirms approval before sending the bridge and observes the original
hash after a lost reply. Status distinguishes the two steps and never exposes
signed bytes. GET /exchange/bridge/active-tron restores the current new bridge
for the authenticated merchant. minimumOutputAmount is the prepared route’s
decoded minimum; expectedOutputAmount remains a prediction. Completion requires
the signed recipient’s actual native USDC credit and destination confirmations.
A transit token returned by the external protocol does not constitute completion.
2026-10-24 — Solana payouts recover a lost wallet acknowledgment
POST /payouts/{id}/prepare returns the fee payer’s stable transaction
signature for Solana. The server retains it before the wallet signs. If the
browser closes before /submitted, the payout worker observes that exact
signature and records the finalized result without sending another transaction.
- Reservation time alone no longer permits another preparation or simulation. Unknown outcomes remain under observation; expiry requires an absent history result after the finalized block height passes the transaction’s validity.
/submittedaccepts a late acknowledgment of that same transaction idempotently, including after confirmation. A different signature is rejected.- Cancelling after handoff does not revoke a wallet request. Rows remain checking until their outcome is established by the network.
2026-10-22 — Mobile API: Convert signing, slippage fixed at 0.5 %
Applies to the mobile API (/mobile/v1, contract 1.0.0-draft.15). The
merchant API is unchanged.
POST /mobile/v1/businesses/{businessId}/operationswithkind: convertaccepts onlyslippageBps: 50for now (omitting it means 50). Any other value answers422 slippage_unsupported. Before this change the contract declared 1–300. The signer computes the minimum you sign against with 0.5 %, so another value would show one minimum in Review and sign a different one. The 1–300 range returns once the signer takes the minimum from Review.POST …/operations/{operationId}/signing-sessionnow opens signing for Convert as well as Send:- the
minimumReceivedshown in Review is the minimum built into the swap; - if a fresh quote would deliver less than that minimum, the answer is
409 price_changedwith the operation and its new Review andreviewHash; no signing session is opened; - a price that falls below the minimum between opening and building the swap is refused before anything is signed or sent.
- the
- Repeating
signing-sessionwith the sameIdempotency-Keyopens a new session and closes the previous one: the server keeps only a hash ofsigningSessionId, so it cannot return the old one.callbackUrlisnullfor now — read the outcome fromGET …/operations/{operationId}.
2026-10-19 — Separate Gateway wallet balances
GET /gateway/accountslists the signed-in merchant’s active balances (id,ownerAddress,chains,status,signableChains).POST /gateway/enableaccepts optionalownerAddressfor an additional wallet. First prove its EOA ownership through/wallets/verification/challenge→/wallets/verification/verify. An unproven address returns403 GATEWAY_OWNER_NOT_VERIFIED. This does not change customer payment addresses or move funds from existing balances.- Pass optional
accountIdin query parameters toGET /gateway/balance,GET /gateway/quote,GET /gateway/transfers, or in the JSON body toPOST /gateway/deposit/prepareandPOST /gateway/transfer/prepare. Use anidreturned by/gateway/accounts. An unknown, inactive or other merchant’s ID returns404 GATEWAY_NOT_ENABLED; malformed IDs return400. Without an ID, existing clients keep the earliest active account. Submit and sync continue using the prepared transfer’s original account.
v0.35.7 — 2026-10-16
Team is part of the Team plan
POST /merchants/team/inviteneeds the Team plan (or agreed custom terms) and holds up to 10 people including the owner. Below Team it answers403 PLAN_REQUIRED; the eleventh person is403 QUOTA_EXCEEDEDwithdetails.quota(metric: "teamMembers",limit,used). Sending the invite again to someone still invited works at the limit — that person is already counted.PATCH /merchants/team/:userId/roletoadminneeds the Team plan; settingvieweralways works.- After a move below Team, members other than the owner act as viewers: they see what they saw and change nothing. Their role is kept and comes back with Team. Nobody is removed.
An unpaid subscription limits new paid actions
- An invoice that is not paid 14 days after it was issued limits new
paid actions to what Starter includes — today that is inviting and promoting
team members and payment requests above the Starter limit. Those answer
403 SUBSCRIPTION_RESTRICTED. - Reading, history, exports, reconciliation of payouts already sent and withdrawals are never limited. Paying the invoice lifts the limit at once.
POST /linkskeeps its own403 LINK_QUOTA_EXCEEDEDfor the Starter limit, and may also answerSUBSCRIPTION_RESTRICTEDwhile an invoice is overdue.
See Errors → Plans.
v0.35.6 — 2026-10-15
A paid plan starts when its invoice is paid
POST /merchants/me/planwith a plan above the current one no longer switches the plan at once. It issues an invoice and answers200 { "applied": "awaiting_payment", "plan", "previousPlan", "amountUsd", "chargeId", "invoiceUrl" }. The account stays on its current plan until the invoice is paid; the new plan starts at the moment of payment, and so does its billing month. There is no proration. Asking for the same plan again returns the same invoice. The invoice is also listed inGET /merchants/me/charges.- Asking for the current plan cancels whatever was planned:
200 { "applied": "unchanged", "cancelled": { "pendingDowngrade": "growth" } }(or"pendingUpgrade"for an unpaid upgrade invoice). Nothing is billed. - One planned change at a time: a new request replaces the previous one, and a paid upgrade cancels a planned downgrade.
- Downgrades are unchanged:
"at_period_end", or"now"when nothing is paid ahead. - New refusal
409 BILLING_UNAVAILABLEwhen an invoice cannot be issued. See Errors → Plans.
Plan catalog
GET /plans— new, public. The plans on sale with their name, monthly price in USD, what they include and their limits. A limit is a number,nullfor no limit, or"contract"for terms agreed individually.
v0.35.5 — 2026-10-10
Sign in with an email code
POST /auth/login-code/request— new:{ "email": "…" }. Sends a 6-digit sign-in code to an active team member of a non-suspended account. The answer is202 { "sent": true }whatever happens on our side, so it does not reveal whether the address has an account. At most 3 emails per address in 15 minutes.POST /auth/login-code/verify— new:{ "email", "code", "team"? }. Answers exactly likePOST /auth/login: a session, ortwoFactorRequired: truewith a token for/auth/2fa/validatewhen two-factor authentication is on — the code proves the inbox, it does not replace the second factor. A wrong or expired code is401 INVALID_LOGIN_CODE; a code works once, for 10 minutes, and is burned after 5 wrong attempts. Repeated failures from one address are429 ACCOUNT_LOCKED.- Passwords keep working; the code is also how a member who forgot the password gets back in.
v0.35.4 — 2026-10-10
Only the owner invites admins
POST /merchants/team/invitewith"role": "admin"now answers403 OWNER_INVITES_ADMINSunless the caller is the account owner. Admins — and API keys, which act with the admin role — can still invite with"role": "viewer". Owners invite admins as before. See Errors → Team for the refusal and the request that is accepted.
v0.35.3 — 2026-09-30
Send a payment request by email
POST /links/{id}/send-email— new:{ "to": "client@example.com" }. Emails the request to your customer from Inzi.201means the email provider accepted it — not that the customer received it.502 EMAIL_NOT_SENTmeans it did not go out; the request is unchanged.GET /links/{id}/email-preview— new: the subject and text thatsend-emailwill send.GET /links/{id}/emails— new: every attempt, newest first, withstatussentorfailed.- Sending needs a person with a confirmed email on the account — API keys
are refused with
403 REQUEST_EMAILS_NOT_AVAILABLE. At most 5 sent emails per request per hour (429 TOO_MANY_EMAILS, withretryAfterSec). GET /links/{id}now carriesmeta.canAcceptAsPaid, the same field the list already had.
v0.35.2 — 2026-09-27
Sign-up is by request for now
POST /auth/registeranswers403 REGISTRATION_BY_REQUESTwhile open sign-up is paused. Existing accounts sign in as before, and team invitations work as before.POST /auth/access-request— new:companyName,email,description. We open the account and the owner gets an email to set a password. The response is202whatever happens on our side.GET /auth/registration— new:{ mode: "open" | "request" }, for your own sign-up screen.
v0.35.1 — 2026-09-25
Disposals: every payout reduces your lots, even before you classify the recipient
- A payout to an address you have not classified used to record nothing
until you answered “whose wallet is this”. It now reduces your lots at
once, as a disposal with
destination: unknown. Answering Someone else changes it tothird_partywithout recalculating; My wallet reverses it and returns the lots. - Payouts since the ledger started (2026-09-03) that were left without a row — unclassified, or deferred because no rate was available — are recorded now, valued at the payout date from the ECB history.
- Transfers you send from a watched wallet outside Inzi (from your own
wallet app) now reduce your lots too, as
source_type: wallet_outflow, valued at the transfer date. Transfers to your other watched wallets are not disposals; Inzi’s own payouts, swaps and bridges are not counted twice. Their recipients join the same “whose wallet is this” list. - Disposals export gains a
destinationcolumn, on the right.
v0.35.0 — 2026-09-27
Inzi takes nothing from exchanges, bridges and Gateway transfers — fee fields removed
Inzi does not charge a fee on exchanges, bridges or unified-balance (Gateway) transfers, and the code that could charge one has been removed. The fields that used to describe that fee are gone from the responses:
- Swap and bridge quotes (
GET /api/v1/exchange/quote,GET /api/v1/exchange/bridge-quote):inziMarkupremoved.totalFeenow holds only the pool or bridge-provider fee. - Gateway (
GET /api/v1/gateway/quote,GET /api/v1/gateway/transfers):fees.inziMarkupremoved.totalisbaseFee + transferFee, both charged by Circle. - Bridge prepare (
POST /api/v1/exchange/bridge/prepare):conversionFeeremoved; the steps to sign are only the provider’s. - Sponsored swap (
POST /api/v1/exchange/swap/sponsored/authorize,/prepare,/submitted):feeremoved. The whole amount goes into the swap, and the errorFEE_CHANGEDno longer occurs. GET /api/v1/merchants/me:swapMarkupBpsremoved.
Migration. These fields were always zero or not_charged for every
merchant since 2026-09-25, so no amount changes. If your integration reads
one of them, treat a missing field as zero — or stop reading it; there is
nothing to replace it with.
v0.34.1 — 2026-09-25
Payment request: who accepted it, and whether a top-up can still arrive
GET /api/v1/links/{id}now returnsrequest, as the list already did.request.acceptedAsPaidAt,request.acceptedAsPaidBy— when and by whom a request was closed with Accept as paid.request.topUpOpenUntil— until when a top-up can still arrive on at least one attempt;nullwhen no window is left.- Deprecated for payment requests:
totalReceivedandpaymentCount. Userequest. - Known inconsistency: the link’s
amountis a number while every other amount is a decimal string. It becomes a string in the next API version.
v0.34.0 — 2026-09-25
”We see your payment”: the detected state is now reported
payment_state.state: "detected"— listed in v0.33.0, now actually reached. A transfer seen on Polygon, Base or Ethereum before it has enough confirmations moves the attempt todetected; the checkout page tells the payer “We see your payment, waiting for the network” instead of asking for the money again.statusstaysprocessing, and no webhook is sent:checkout.completedstill arrives only once the transfer is confirmed.POST /api/v1/checkout/verify-tx/{paymentId}— a transaction hash that is valid but not yet confirmed used to be refused with400 VERIFICATION_FAILED/unconfirmed. It is now accepted with202 { "accepted": true, "detected": true }, and the hash is re-checked until the network confirms it. No need to resubmit.- A transfer that was seen and never confirmed (a reorg, a dropped
transaction) ends as
expiredwhen the search window closes, withcheckout.expired, exactly as if nothing had arrived.
v0.33.0 — 2026-09-24
Partial payments: no tolerance, remaining amount, accept as paid
Behaviour change for partial payments. Read before this release reaches you.
- No tolerance. A payment used to count as complete within max(1%, $0.05) of the price. It now counts as complete only when the full price arrives. Less is a partial payment, and the difference stays visible.
checkout.partially_paid— new event. When the search window (30 minutes afterexpires_at) closes with part of the price received, the attempt is no longer marked expired. You receivecheckout.partially_paidwithreceived_amountandremaining_amount. Before, this case sentcheckout.expired.checkout.expirednow means only that nothing arrived.- Remaining amount. A fixed-price checkout that was partly paid stays open: a new attempt asks only for the remaining amount, and the checkout completes when the total over all attempts reaches the price.
- Accept as paid. An owner or admin can close a partially paid checkout
as paid from the dashboard. You receive
checkout.completedwithaccepted_as_paid: true,received_amountandshortfall_amount. GET /api/v1/checkouts/{id}gainspayment_state:state(awaiting/detected/paid/partially_paid/overpaid),amount_received,amount_remaining,surplus,accepted_as_paid, summed over all attempts.statusfollows the checkout, not the last attempt:completedonce paid in full or accepted.
See Webhooks → Partial payments for payloads.
v0.32.0 — 2026-09-24
Payer consent: one to one, not “converted at the rate”
Payments are now at par: a USD price is paid in USDC or USDT, one to one, and a EUR price in EURC, one to one. Cross pairs (EURC against USD, USDC or USDT against EUR) are not accepted.
The consent wordings v1 and v2 said the amount “is converted at the rate
shown at the time of payment”. At par that is not true, and it is the text the
payer confirms. They are replaced by new wordings:
acceptanceMode | was | now |
|---|---|---|
checkbox | v1 | v3 — “I agree to pay in USDC instead of USD, one to one: 1 USDC for each 1 USD of the price. I understand that USDC is not legal tender.” |
continue | v2 | v4 — “By continuing, you agree to pay in USDC instead of USD, one to one: 1 USDC for each 1 USD of the price. USDC is not legal tender.” |
Spanish texts change the same way.
From this release, POST /api/v1/checkout/{short_code}/pay accepts only
v3 / v4. A request still sending v1 / v2 is refused with
CONSENT_FORM_MISMATCH.
If your integration reads the wording from GET /api/v1/checkout/{short_code}
→ consent.forms (or consent.version) and sends back the version it
showed, nothing changes for you: the new wording arrives by itself. If the
version is hardcoded, replace it with the one from consent.forms.
- Consents already recorded with
v1/v2keep their exact text as evidence; nothing recorded is rewritten. GET /checkout/{short_code}lists inacceptedTokens, and gives consent texts for, only tokens that settle the link’s currency one to one.- A cross pair on
/payis refused withTOKEN_CURRENCY_MISMATCH, whose message now names the rule and where to pick a token. No consent is recorded for a payment that is refused this way.
The merchant’s books are unchanged: a dollar payment is still valued in euros at the real rate when it arrives. One to one is the price for the payer, not the accounting.
v0.31.0 — 2026-09-24
Disposals export: unknown is empty, not zero
GET /api/v1/reports/disposals (JSON and CSV) changes so that the export can
be filed from without knowing how Inzi works. The meaning of every column is
on the new Disposals export page.
Breaking: fields and columns were renamed, and two fields can now be null.
costBasisEur/cost_basis_eurandrealizedGainEur/realized_gain_eurare empty (nullin JSON) unless the cost basis is known for the whole amount. Before, they held the cost of the covered part only and a gain overstated by the rest.- When the cost basis is partly known, the gain on the known part is in new
fields:
coveredAmount,coveredProceedsEur,coveredCostBasisEur,coveredRealizedGainEur(covered_*in CSV). meta.totalRealizedGainEurnow sums only known gains: full rows plus the covered part of partial rows.costBasisCompleteanduncoveredReasonare replaced bycostBasisStatus(CSV:cost_basis_complete→cost_basis_status) with the same values.proceedsEstimatedis replaced byrateStatus:spot_at_recordingorhistorical_estimate(CSV:proceeds_estimated→rate_status, which used to readlocked/estimated).proceedsRateAtis renamedrateAt.- New fields:
receivedToken,receivedAmount,network,walletAddress,txHash,feeToken,feeAmount,feeEur. In CSV they are appended on the right; existing columns keep their positions.
v0.30.0 — 2026-09-23
Direct transfers to your wallets now count as lots
Transfers that reach your wallets outside Inzi — not through a payment link — are now recorded with the exchange rate at the moment we detect them, and a disposal from that wallet draws on them like on any payment.
Transfers that arrived before we watched the wallet are recorded as facts (amount, hash, block, time) without a rate: a rate at that past moment cannot be recovered honestly.
Lots without a known rate now keep their place in the FIFO queue. Before, they were skipped, and a newer lot with a rate could be drawn first, putting a confident but wrong cost basis on the disposal.
GET /api/v1/reports/disposals:
- each item in
lotsmay carrywalletTransactionId(a direct transfer) instead ofpaymentId; lots[].costBasisEurisnullwhen the lot has no known rate;uncoveredReasongainsrate_and_source_unknown, and the CSVcost_basis_completecolumn can show the same value.
Transfers between your own wallets are not yet linked: the receiving wallet records a new lot at the rate on arrival, and the sending wallet’s lot stays open.
v0.29.0 — 2026-09-23
Disposals are matched only against the wallet they left
Behaviour change in cost basis, additive in shape.
A disposal (swap, payout, bridge) is now matched only against payments received on the same wallet on the same network. Before, it drew on any payment the merchant had received in that token.
Why: Inzi sees only part of a merchant’s coins. Funds sent straight to a wallet, bypassing Inzi, never enter our records — so matching across wallets could pin the cost basis of a real customer payment on coins that arrived from somewhere else. That figure looked certain and was wrong. Tax FIFO runs across all of a taxpayer’s holdings; the full calculation belongs to your accountant or a crypto-tax tool. What Inzi can state reliably is what happened on each wallet it knows.
GET /api/v1/reports/disposals adds uncoveredReason per row:
| Value | Meaning |
|---|---|
rate_unknown | This wallet received payments in this token before rate capture — their cost basis was never recorded |
source_unknown | No payment on this wallet covers it — the asset arrived outside Inzi |
null | Fully covered |
In ?format=csv, cost_basis_complete can now be complete, rate_unknown,
source_unknown, or reason_not_recorded — the last only for a row written
before reasons existed. Column positions are unchanged.
v0.28.0 — 2026-09-23
Disposals now say when their cost basis is incomplete
Additive — nothing breaks. New fields only; existing ones keep their meaning and, in the CSV, their column positions.
FIFO builds lots only from payments whose exchange rate was recorded. A payment received before rate capture existed has no euro value, and inventing one after the fact would put a made-up number in your accountant’s report. So those payments are skipped — which means a disposal covering them has a cost basis for only part of the amount, and a realized result overstated by the rest.
Until now that was a line in our logs and nothing else: the exported row looked complete.
GET /api/v1/reports/disposals adds two fields per row:
| Field | Meaning |
|---|---|
uncoveredAmount | How much of the disposal no priced lot covered |
costBasisComplete | false when the row’s cost basis is partial |
and two to meta:
| Field | Meaning |
|---|---|
uncoveredCount | Rows whose cost basis is partial |
uncoveredAmount | Total uncovered across them |
While uncoveredCount is above zero, totalRealizedGainEur is an upper
bound, not a final figure.
?format=csv gains uncovered_amount and cost_basis_complete
(complete / rate_unknown) appended at the end of the row. Existing
columns did not move, so a parser reading by position keeps working.
There is nothing to fix on your side: a rate that was never recorded cannot be recovered later. The fields exist so the gap is visible instead of silent.
v0.27.0 — 2026-09-23
consent.version is now required, and both wordings are published
Breaking for integrations that render their own payment screen. If you
redirect payers to checkout_url, nothing changes.
POST /api/v1/checkout/{short_code}/pay no longer guesses which wording the
payer read. Send consent.version — the one you actually displayed. A consent
record naming wording the payer never saw is not consent, and the server has
no way to know what your screen showed.
Missing the field returns 400 CONSENT_VERSION_REQUIRED with
details.expected.
GET /api/v1/checkout/{short_code} now publishes every form this merchant
may use, each with its own ready-made texts per token:
"consent": {
"version": "v2", "acceptanceMode": "continue", "locale": "en",
"texts": { "USDC": "By continuing, you agree to pay in USDC instead of USD. …" },
"forms": [
{ "acceptanceMode": "continue", "version": "v2",
"texts": { "USDC": "By continuing, you agree to pay in USDC instead of USD. …" } },
{ "acceptanceMode": "checkbox", "version": "v1",
"texts": { "USDC": "I agree to pay in USDC instead of USD. …" } }
]
}Use the text from forms verbatim and send back the version of the form you
displayed. Do not hard-code the wording: it is versioned, and a change reaches
your payers only if you read it from here.
continue appears only for merchants that offer a non-crypto alternative —
it asserts the payer had a choice. checkbox is always available.
v0.26.0 — 2026-09-04
Payer consent is required to start a payment
POST /api/v1/checkout/{short_code}/pay now requires a consent object. It
records that the payer was shown, and accepted, the text explaining they are
paying in stablecoin.
If you render your own payment screen — a Mini App, a bot, a custom checkout —
you must show that text and send the field. Our hosted checkout page does
this for you, so nothing changes if you redirect payers to checkout_url.
POST /api/v1/checkout/aBcDeFgHiJ/pay
{
"chain": "base",
"token": "USDC",
"consent": { "accepted": true, "locale": "en", "acceptanceMode": "checkbox" }
}acceptanceMode is checkbox unless the merchant offers a non-crypto
alternative, in which case continue is also accepted. Omitting the field
returns 428 CONSENT_REQUIRED; the response carries details.expected with
the exact shape to send.
Documented late. The requirement went live on 2026-09-04 and this entry
was written on 2026-09-22, after an integration spent a month unable to take
crypto orders. Calls without consent had been failing since the former
date. We are sorry — and the rule that produced the gap is now ours to keep:
a change to what a public endpoint requires gets an entry here before it
ships.
v0.25.0 — 2026-08-21
Per-invoice addresses on Base and Polygon
A checkout on these chains now gets its own deposit address, so the customer
pays the round price — no fractional suffix to reproduce. Nothing changes in
how you call the API; POST /v1/checkouts is unchanged and the deposit address
still arrives the same way.
Everywhere else a checkout is still identified by its exact amount. GET /v1/chains reports this per chain as exactAmountRequired, and
available_chains on a checkout is sorted so chains taking the round price
come first. If you build your own network selector, sort by priority and show
a warning where exactAmountRequired is true.
References: customer_ref, product_ref, order_ref
Three optional strings on POST /v1/checkouts, also read from metadata.
They are opaque to us — compared, never interpreted.
order_refmakes creation idempotent. The same order, amount and currency returns the existing checkout instead of a second one. A changed amount always creates a new checkout, so a re-priced order can never show a buyer one figure and charge another.customer_ref+product_refrescue payments that would otherwise go unattributed: where the amount identifies the invoice, several checkouts at one price are indistinguishable if the customer pays the round figure. With both references we can tell they are the same person buying the same thing and credit one. Both are required — two products can cost the same and be different things.
Fully backwards compatible: send nothing and behaviour is exactly as before.
v0.10.1 — 2026-04-07
Security Hardening
- x402 verifiers — stricter on-chain validation (recipient, token contract, BigInt amounts)
- Auth —
requireRole()fix, timing-safe password comparison, login lockout (5 attempts → 15 min) - CSP — removed
unsafe-eval - SSRF — webhook URLs to private IPs blocked
- Rate limits — payment status polling + admin seed endpoints
v0.10.0 — 2026-04-06
x402 Facilitator
POST /v1/x402/verify— synchronous on-chain payment verification across 6 chainsGET /v1/x402/config— facilitator payTo addresses, supported networks/assetsPOST /v1/x402/settle— trigger settlement of x402 paymentsGET /v1/x402/payments— list merchant’s x402 payments- @inzi/x402 SDK — Express middleware + Fastify plugin (zero deps, native fetch)
- Supported chains: Polygon, Base, Ethereum, Tron, TON, Solana
Developer Portal
- docs.inzilink.com — Nextra 4 docs site with 22 pages
- API reference, guides, SDK examples, x402 integration guide
v0.9.0 — 2026-04-05
Wallet Monitoring & Reports
/api/v1/wallets— CRUD for watched wallet addresses (Polygon/Base/Ethereum/Tron/TON/Solana)- Balance fetching — EVM + TronGrid + TON Center, Redis cache 60s, background worker
- Transaction sync — Etherscan/Polygonscan/Basescan/TronScan/TON Center (5min cycle)
- Auto-tagging — matches tx hashes against Inzi payments (payment/sweep/settlement)
- Manual labels — user-defined tags on transactions
/api/v1/reports/summary— aggregate balances, inflow/outflow, tag breakdown/api/v1/reports/transactions— cross-wallet tx list (JSON + CSV export)
Bug Fixes
- TON added to checkout chain validation (was missing from Zod enum)
- Tron added to Merchant API checkout accepted chains
v0.8.0 — 2026-04-04
Rebrand & Domain
- Inzilink → Inzi — brand name updated across all UI, emails, schema.org
- Primary frontend domain:
inzipay.com - API domain unchanged:
api.inzilink.com - Payment links unchanged:
inzilink.com/p/{code}
Corrected 2026-09-06: this line named a path that never shipped. The hosted checkout has always been served at/c/{code}, onwww.inzipay.com.
Moved 2026-09-07:checkout_urlnow returnshttps://pay.inzilink.com/c/{code}. Links already issued onwww.inzipay.comkeep working — that host still serves the same checkout.
Fiat Waitlist
POST /api/v1/waitlist/fiat— capture card payment interest
v0.7.0 — 2026-04-03
Merchant Checkout API
POST /v1/checkouts— create checkout session, returns hostedcheckout_urlGET /v1/checkouts/:id— status + payment details withchk_live_/chk_test_prefixed IDsGET /v1/checkouts— paginated list with status/date filters- Metadata passthrough — arbitrary JSON (max 4KB), returned in webhooks untouched
- Inzi webhook format — timestamp-based HMAC (
X-Inzi-Signature), events:checkout.completed,checkout.expired,checkout.underpaid - Test mode —
sk_test_keys fire mock webhook after 5s, no blockchain monitoring - Stripe-like errors —
{ error: { type, message, param, code } } - Per-endpoint rate limits — 60/30/120 req/min with
X-RateLimit-*headers - redirect_url — auto-redirect 2s after payment success
- Amount tolerance —
max(1%, $0.05)for network rounding
TON Blockchain
- Chain.TON — The Open Network added as supported chain
- TonTheScanner — Jetton (USDT) transfer monitoring via TON Center v3 API
- TON wallet — V4R2 hot wallet on mainnet, memo-based addressing
v0.6.0 — 2026-03-30
Merchant Experience
- Payment link templates: invoice, donation, subscription, custom
- Custom branding on checkout (logo, color, support URL)
- CSV export for payments and settlements
- Daily email summary (08:00 UTC)
- Multi-user merchant accounts (owner/admin/viewer roles)
- Team management API and invite flow
v0.5.0 — 2026-03-30
Production Hardening
- Webhook retry dashboard + manual retry
- Sentry error monitoring
- PaymentRouter verified on Polygonscan and BaseScan
- Uptime monitoring
v0.4.0 — 2026-03-28
Non-Custodial Escrow
- PaymentRouter contract on Polygon
- Dual payment mode: escrow (non-custodial) + custodial fallback
- Wallet generation endpoint
- Auto on-chain merchant registration
v0.3.0 — 2026-03-22
Admin & Billing
- Admin panel for merchant management
- Fee system (starter 1.5%, growth 1.0%, scale 0.7%, custom)
- Subscription billing
v0.2.0 — 2026-03-15
Frontend & Deployment
- Merchant dashboard (Next.js)
- Checkout page with QR code
- Landing page
- E2E + integration tests
- Railway (API) + Vercel (web) deployment
- API key generation (
sk_live_prefix)
v0.1.0 — 2026-03-11
Initial MVP
- Monorepo (pnpm workspaces)
- Fastify 5 API with versioned routes
- Prisma 6 + PostgreSQL
- JWT + API key authentication
- EVM scanner (Polygon, Base, Ethereum)
- TronScanner (TRC-20 USDT)
- BullMQ workers (scanner, payment, webhook, sweep, settlement)