Skip to Content
DocumentationChangelog

Changelog

All notable API changes documented here.


2026-10-27 — Business time zone and monthly totals

  • PATCH /api/v1/merchants/me accepts timezone, an IANA name such as Europe/Madrid.
    • The server stores the canonical spelling: europe/madrid is saved as Europe/Madrid.
    • An empty string clears the time zone.
    • Fixed offsets (+01:00, Etc/GMT+3) and unknown names return 400.
    • GET /api/v1/merchants/me returns timezone, or null when it is not set.
  • You can also set it in the dashboard under Settings → Time zone.
  • Mobile API (/mobile/v1, contract 1.0.0-draft.21):
    • GET /me returns each business’s timezone.
    • GET …/home returns month when 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.totals has 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, month stays null.

2026-10-26 — Mobile API: push devices

Applies to the mobile API (/mobile/v1, contract 1.0.0-draft.20).

  • POST /mobile/v1/devices registers 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 201 with the same deviceId that 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 returns 204. Another person’s device or an unknown device returns 404.
  • 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}/recipients lists saved recipients, recently used first. Search with q by name or address.
  • POST …/recipients saves 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:
    • archived is always false;
    • the similar_to_saved warning 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-requests creates a payment request.
    • The asset of amount sets 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 payUrl to pay.
    • Repeating the call with the same Idempotency-Key returns 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.
  • GET …/payment-requests lists requests newest first, with a cursor and a status filter: open, partly_paid, paid, overpaid, accepted_as_paid, expired, cancelled.
  • GET …/payment-requests/{requestId} returns one request with paid, the amount actually received.
  • POST …/payment-requests/{requestId}/accept-as-paid closes a partly paid request as paid. It is idempotent by key and answers 409 invalid_state when the request is not partly_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}/activity is 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.
  • A transfer that is both a payment and a wallet movement appears once.
  • Newest first. cursor / nextCursor stay stable when new entries arrive between pages.
  • Search with q by name, address, request reference or transaction hash. Filter by direction, status, currency, walletId, from / to.
  • GET …/activity/{itemId} returns one entry.
  • GET …/home: recent now holds the latest five entries of the same feed. It is [] when there are none.
  • Not yet: suspicious-incoming detection. suspicious is always false, hiddenCount is always 0, and the needs_review status does not occur. POST …/needs-you/{itemId}/resolve answers 409 invalid_state for items that clear themselves and 404 otherwise.

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 operation submitted → unknown itself. Show “outcome unknown” with POST …/check-status instead of an endless spinner.
  • Nothing is sent again from unknown. The server keeps looking for the outcome; check-status picks it up. The Needs you item operation_unknown stays until the operation is confirmed or failed.

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.
  • /submitted accepts 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}/operations with kind: convert accepts only slippageBps: 50 for now (omitting it means 50). Any other value answers 422 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-session now opens signing for Convert as well as Send:
    • the minimumReceived shown in Review is the minimum built into the swap;
    • if a fresh quote would deliver less than that minimum, the answer is 409 price_changed with the operation and its new Review and reviewHash; 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.
  • Repeating signing-session with the same Idempotency-Key opens a new session and closes the previous one: the server keeps only a hash of signingSessionId, so it cannot return the old one. callbackUrl is null for now — read the outcome from GET …/operations/{operationId}.

2026-10-19 — Separate Gateway wallet balances

  • GET /gateway/accounts lists the signed-in merchant’s active balances (id, ownerAddress, chains, status, signableChains).
  • POST /gateway/enable accepts optional ownerAddress for an additional wallet. First prove its EOA ownership through /wallets/verification/challenge → /wallets/verification/verify. An unproven address returns 403 GATEWAY_OWNER_NOT_VERIFIED. This does not change customer payment addresses or move funds from existing balances.
  • Pass optional accountId in query parameters to GET /gateway/balance, GET /gateway/quote, GET /gateway/transfers, or in the JSON body to POST /gateway/deposit/prepare and POST /gateway/transfer/prepare. Use an id returned by /gateway/accounts. An unknown, inactive or other merchant’s ID returns 404 GATEWAY_NOT_ENABLED; malformed IDs return 400. 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/invite needs the Team plan (or agreed custom terms) and holds up to 10 people including the owner. Below Team it answers 403 PLAN_REQUIRED; the eleventh person is 403 QUOTA_EXCEEDED with details.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/role to admin needs the Team plan; setting viewer always 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 /links keeps its own 403 LINK_QUOTA_EXCEEDED for the Starter limit, and may also answer SUBSCRIPTION_RESTRICTED while 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/plan with a plan above the current one no longer switches the plan at once. It issues an invoice and answers 200 { "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 in GET /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_UNAVAILABLE when 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, null for 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 is 202 { "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 like POST /auth/login: a session, or twoFactorRequired: true with a token for /auth/2fa/validate when two-factor authentication is on — the code proves the inbox, it does not replace the second factor. A wrong or expired code is 401 INVALID_LOGIN_CODE; a code works once, for 10 minutes, and is burned after 5 wrong attempts. Repeated failures from one address are 429 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/invite with "role": "admin" now answers 403 OWNER_INVITES_ADMINS unless 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. 201 means the email provider accepted it — not that the customer received it. 502 EMAIL_NOT_SENT means it did not go out; the request is unchanged.
  • GET /links/{id}/email-preview — new: the subject and text that send-email will send.
  • GET /links/{id}/emails — new: every attempt, newest first, with status sent or failed.
  • 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, with retryAfterSec).
  • GET /links/{id} now carries meta.canAcceptAsPaid, the same field the list already had.

v0.35.2 — 2026-09-27

Sign-up is by request for now

  • POST /auth/register answers 403 REGISTRATION_BY_REQUEST while 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 is 202 whatever 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 to third_party without 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 destination column, 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): inziMarkup removed. totalFee now holds only the pool or bridge-provider fee.
  • Gateway (GET /api/v1/gateway/quote, GET /api/v1/gateway/transfers): fees.inziMarkup removed. total is baseFee + transferFee, both charged by Circle.
  • Bridge prepare (POST /api/v1/exchange/bridge/prepare): conversionFee removed; the steps to sign are only the provider’s.
  • Sponsored swap (POST /api/v1/exchange/swap/sponsored/authorize, /prepare, /submitted): fee removed. The whole amount goes into the swap, and the error FEE_CHANGED no longer occurs.
  • GET /api/v1/merchants/me: swapMarkupBps removed.

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 returns request, 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; null when no window is left.
  • Deprecated for payment requests: totalReceived and paymentCount. Use request.
  • Known inconsistency: the link’s amount is 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 to detected; the checkout page tells the payer “We see your payment, waiting for the network” instead of asking for the money again. status stays processing, and no webhook is sent: checkout.completed still 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 with 400 VERIFICATION_FAILED / unconfirmed. It is now accepted with 202 { "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 expired when the search window closes, with checkout.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 after expires_at) closes with part of the price received, the attempt is no longer marked expired. You receive checkout.partially_paid with received_amount and remaining_amount. Before, this case sent checkout.expired.
  • checkout.expired now 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.completed with accepted_as_paid: true, received_amount and shortfall_amount.
  • GET /api/v1/checkouts/{id} gains payment_state: state (awaiting / detected / paid / partially_paid / overpaid), amount_received, amount_remaining, surplus, accepted_as_paid, summed over all attempts. status follows the checkout, not the last attempt: completed once paid in full or accepted.

See Webhooks → Partial payments for payloads.


v0.32.0 — 2026-09-24

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:

acceptanceModewasnow
checkboxv1v3 — “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.”
continuev2v4 — “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 / v2 keep their exact text as evidence; nothing recorded is rewritten.
  • GET /checkout/{short_code} lists in acceptedTokens, and gives consent texts for, only tokens that settle the link’s currency one to one.
  • A cross pair on /pay is refused with TOKEN_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_eur and realizedGainEur / realized_gain_eur are empty (null in 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.totalRealizedGainEur now sums only known gains: full rows plus the covered part of partial rows.
  • costBasisComplete and uncoveredReason are replaced by costBasisStatus (CSV: cost_basis_complete → cost_basis_status) with the same values.
  • proceedsEstimated is replaced by rateStatus: spot_at_recording or historical_estimate (CSV: proceeds_estimated → rate_status, which used to read locked / estimated). proceedsRateAt is renamed rateAt.
  • 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 lots may carry walletTransactionId (a direct transfer) instead of paymentId;
  • lots[].costBasisEur is null when the lot has no known rate;
  • uncoveredReason gains rate_and_source_unknown, and the CSV cost_basis_complete column 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:

ValueMeaning
rate_unknownThis wallet received payments in this token before rate capture — their cost basis was never recorded
source_unknownNo payment on this wallet covers it — the asset arrived outside Inzi
nullFully 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:

FieldMeaning
uncoveredAmountHow much of the disposal no priced lot covered
costBasisCompletefalse when the row’s cost basis is partial

and two to meta:

FieldMeaning
uncoveredCountRows whose cost basis is partial
uncoveredAmountTotal 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

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_ref makes 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_ref rescue 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 chains
  • GET /v1/x402/config — facilitator payTo addresses, supported networks/assets
  • POST /v1/x402/settle — trigger settlement of x402 payments
  • GET /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}, on www.inzipay.com.
    Moved 2026-09-07: checkout_url now returns https://pay.inzilink.com/c/{code}. Links already issued on www.inzipay.com keep 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 hosted checkout_url
  • GET /v1/checkouts/:id — status + payment details with chk_live_/chk_test_ prefixed IDs
  • GET /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)
Last updated on