Journal des modifications

Chaque changement d'API, breaking ou additif. Tagué avec le commit de déploiement (voir GIT_REV dans les en-têtes de réponse).

breaking

Pricing update: minimum payin is now 5 USDT, network fee is now $1.50

The minimum payin amount on USDT TRC-20 is now 5 USDT (was 1 USDT) and the per-payment network fee is now $1.50 (was $1.00). Both apply to API payins and hosted paylinks. The merchant fee (1.5%) and all other rail fees are unchanged. Customers paying through a hosted paylink see the new minimum and network fee live; merchants do not need to update anything. Refresh GET /v1/config in your integration to pick up the new values, or hardcode them per the table in the docs.

fixed

Paylink checkout: removed the 5-minute price-lock countdown

We removed the 5-minute price-lock countdown on paylink checkouts. The displayed total is still locked at click time and re-verified server-side, so click-time total continues to equal charged total — your customers just see one less timer on the screen.

added

Smart checkout: one page, one final price

The paylink checkout now shows products and the payment method on the same screen, with the final amount (fees included) updating live as the customer chooses. The total displayed at click time is the exact total charged — locked client-side and re-verified server-side. Merchants can also pin a default method per paylink (Default method on checkout in the paylink editor); customers can still switch on the page. Existing paylinks behave exactly as before until you set a default.

added

Paylinks: paiements multi-devises

Vos paylinks acceptent maintenant tous les moyens de paiement que vous avez activés, quelle que soit la devise affichée. Si un client choisit Mobile Money ou Carte sur un lien libellé en USDT (ou inversement), le montant est converti au taux du jour et affiché dans la devise du moyen choisi avant validation. Vous recevez les fonds dans la devise native du rail (USDT pour USDT, FCFA pour Mobile Money et Carte). Aucune action requise — le changement est automatique sur tous vos liens existants. Un nouvel endpoint public POST /v1/paylinks/:slug/rate-lock verrouille le taux 5 minutes côté client pour éviter toute dérive entre l'affichage et la confirmation.

added

Bulk payments: FCFA Mobile Money rail

Bulk payments now support a second rail: FCFA Mobile Money via Burkina Faso operators (Orange, Moov). Pick the rail when you create a batch — USDT batches still use the same TRON flow as before. Each rail has its own Excel template; you cannot mix rails in one batch.

fixed

Card payments: minimum amount lowered from 500 XOF to 100 XOF

The minimum amount for a card pay-in (`POST /v1/card-payments`) is now 100 XOF, down from 500 XOF. Sub-100 XOF amounts still return `AMOUNT_BELOW_MINIMUM` with the new threshold. Mobile-money pay-ins still require 500 XOF (carrier-imposed). No SDK or contract changes — just a lower floor on the amount field.

fixed

Dashboard analytics: production-only volume + native-currency totals

The merchant dashboard analytics tiles (Volume, Daily Volume, Status Counts, Summary) now exclude sandbox transactions by default — previously, test-mode payments leaked into your live analytics so the Volume tile and the underlying `GET /v1/analytics/volume` and `/v1/analytics/summary` endpoints could mix real and test totals. The headline `volume` field is also no longer hardcoded to USDT: a merchant taking only XOF (MoMo / card) used to see "0" on the dashboard even when the per-currency breakdown had the real number. The response now reports a `primaryCurrency` (the currency you do most of your business in, by transaction count) and a `primaryVolume` figure, and the legacy `volume` field mirrors `primaryVolume` so existing integrations keep working but on the right number. The summary endpoint additionally exposes a `totalVolumeByCurrency` map so multi-currency UIs can render each rail separately instead of summing across units.

fixed

Merchant API DX cleanup: transactions type filter, whitelist availableAt, docs reconciled

`GET /v1/transactions?type=WITHDRAWAL` is now accepted by the validator (previously rejected as "must be one of PAYIN, PAYOUT, DEPOSIT" — even though the docs and the unified-history service path already supported `WITHDRAWAL`). The single-address `POST /v1/wallets/whitelist` response now includes `availableAt` (24h cooldown end), matching the bulk endpoint so merchants don't have to compute it from `createdAt`. Docs corrected to match runtime: Transactions Export is documented as the async job it actually is (`{jobId, status}` → poll `GET /v1/exports/:jobId/status` → fetch `/download`); the wallets section explains there are three wallets (USDT + one XOF wallet per fiat rail, currently `MOMO` and `CARD`) and the TypeScript examples filter by the `(currency, rail)` pair so the second XOF wallet stops being silently invisible; `POST /v1/payout/estimate` parameter table now correctly marks `destinationAddress` as required; webhook endpoint docs correctly reference the response field as `secret` (with `whs_` prefix) — copying the old `endpoint.signingSecret` example used to set the env var to `undefined` and break signature verification — and document the actual flat retry shape (`maxRetries` default 3, `retryBackoff` enum, `retryIntervalSeconds`).

added

Transactions: customer column + search by email/name/phone

The merchant Transactions list (dashboard) now shows a "Customer" column — full name, or a masked email (`jo***@gmail.com`) when only the email is known, or "Unknown customer" when no payer details were captured. Hover any row to see the full name/email/phone in a tooltip. A new search box on the same page filters by customer email, customer name, customer phone, your `externalRef`, or the merchant note (case-insensitive, debounced). Same `search` query parameter is now accepted on `GET /v1/transactions` so SDK and direct API consumers can build the same lookup. The transaction detail page already showed customer info for paylink-collected payments; it now also reads from the unified customer record so card/USDT/MoMo rails render consistently. No schema change, no new pricing — purely a read/UI surface upgrade.

fixed

SDK 1.7.4 — sandbox routing now automatic

All three SDKs (TS, Python, PHP) now route to sandbox automatically when the API key starts with `zyp_test_`. Before 1.7.4, calling `paylinks.create({...})` with a sandbox key would fail with `SANDBOX_KEY_LIVE_REQUEST` because the SDK had no way to send the `?sandbox=true` flag — leaving every documented sandbox sample unusable. The client now injects the flag on every request when it detects a test-prefixed key. Pass an explicit `sandbox: true|false` (TS/Python) or `'sandbox' => true|false` (PHP option) to override the auto-detection if you need to.

fixed

Quickstart cURL/SDK samples now use the real paylink schema

The Quickstart page was shipping a payload (`amount`, `currency: "USDT"`, `description`, `customer_email`) that the validator rejects on every field — copy-pasting the cURL would 400 with `products must contain at least 1 elements`. Samples now match the actual `POST /v1/paylinks` contract: a `products[]` array with `name` and `price`, plus `currency: "USDT_TRC20"` (or `XOF` for CFA Franc paylinks). The `x-sandbox` header has been dropped from the cURL — `?sandbox=true` is the canonical sandbox flag. TypeScript, Python, and PHP samples updated to read `link.paymentUrl` (the real response field), not the older `link.checkout_url`.

added

Time-based promo code expiry

Paylink promo codes now support an optional `expiresAt` (ISO 8601 timestamp). Useful for seasonal or event-tied promotions — Tabaski, end-of-month sales, Black Friday — where the discount should stop applying automatically without remembering to disable it. Pair with `maxUses` (count limit) or use either independently. When a customer tries to redeem an unusable code, the API now returns one of four distinct error codes — `PROMO_CODE_INVALID`, `PROMO_CODE_INACTIVE`, `PROMO_CODE_EXPIRED`, `PROMO_CODE_USAGE_LIMIT_REACHED` — so checkout UIs can show a tailored message ("This code has expired" vs "This code has been used too many times") instead of a generic validation error. SDKs 1.7.3 (TS, Python, PHP) expose the new field on `createPromoCode()` and the response type, plus the four error code constants.

fixed

Sandbox key now works on every paylink sub-resource (no flag needed)

Previously, calling any POST endpoint nested under `/v1/paylinks/{id}/...` (promo codes, save-as-template, products, cover-image, subscription cancel/pause/resume, etc.) with a `zyp_test_sk_…` key returned `SANDBOX_KEY_LIVE_REQUEST` unless you also added `?sandbox=true`. The check has been narrowed to the actual creation endpoints — `POST /v1/payments` and `POST /v1/paylinks`. Sub-resources of an existing paylink now inherit sandbox status from the parent, so test keys can call them without the flag. SDKs 1.7.2 also accept a `sandbox: true` option on `paylinks.createPromoCode()` and `paylinks.saveAsTemplate()` for backward compatibility.

fixed

SDK 1.7.1 — payins.simulate() return type

The TypeScript SDK typed `payins.simulate(id)` as returning a `Payin`, but the API actually returns an acknowledgement: `{ message, transactionId }`. Calls would resolve with `status: undefined` and the wrong shape. Fixed in @zyndpay/sdk 1.7.1 with a new `SimulatePayinResponse` type — simulate still triggers the `payin.confirmed` webhook server-side; fetch the post-confirmation state with `payins.get(id)`. zyndpay 1.7.1 (PyPI) and zyndpay/zyndpay-php 1.7.1 (Packagist) docstrings updated to clarify the same field shape.

added

Python + PHP SDK 1.7.0 — full webhook event coverage (40 events)

zyndpay 1.7.0 (PyPI) and zyndpay/zyndpay-php 1.7.0 (Packagist) now expose typed constants for every webhook event the API delivers: deposit.failed, withdrawal.requested/approved, all conversion.* (2), all subscription.* (8), all refund.* (5), all dispute.* (4), aml.flagged, and splitpayment.created — 24 new constants per SDK on top of the existing 16. Both SDKs also ship an ALL_EVENTS list for subscribing an endpoint to every event in one call. Webhook delivery and signature verification are unchanged; this only fixes the missing typed-constant surface so PHP/Python integrators no longer have to fall back to raw strings.

fixed

Documentation accuracy: webhook event names, MoMo amount currency, sandbox key prefix

Three documentation inconsistencies fixed across the API Reference, Webhooks, and Sandbox pages: (1) the success event for pay-ins is `payin.confirmed`, not `payin.succeeded`; the JSON envelope field is `event`, not `event.type`. The Webhooks page event catalog now lists all 40 events the API actually delivers. (2) The Mobile Money `amount` is in XOF as an integer-valued string (no decimals), not USDT. The Card section was already correct; the MoMo section now matches. (3) Sandbox API keys use the `zyp_test_sk_…` prefix (not `zpk_sandbox_…`). The key prefix alone does not auto-route — pair it with `?sandbox=true` on the URL or `x-sandbox: true` (not `x-zyndpay-sandbox`) on payment-creation requests.

fixed

Webhook coverage: payin.failed for card + MoMo, payin.confirmed delivery, subscription.created

Three gaps closed: (1) `payin.failed` now fires across all rails — card declines, mobile money provider failures, and USDT dust-rejections — with a `reason` field carrying the operator-readable cause. (2) `payin.confirmed` for card and mobile money pay-ins is now delivered through the standard webhook pipeline; previously the card path enqueued the wrong job shape and the MoMo path emitted only an internal event, so merchants subscribed to `payin.confirmed` on either rail were silently receiving nothing. (3) `subscription.created` now fires when a recurring subscription is initiated on a paylink checkout (the event existed in the schema but was never emitted). Documentation and a published fee schedule shipped alongside.

added

Hosted MoMo checkout — redirect to `hostedPaymentUrl`

POST /v1/payments with paymentMethod=MOBILE_MONEY now returns hostedPaymentUrl. Redirect your customer there and ZyndPay handles the OTP form, the operator instruction, and the status polling on a ZyndPay-branded page. The legacy direct flow (nextStep / operatorCode / instruction + Submit OTP) is still supported for merchants who want full control of the UI, but new integrations should prefer the hosted flow.

added

24h address cooldown lifted for API-key withdrawals

POST /v1/withdrawals no longer enforces the 24-hour cooldown on newly added whitelist addresses when called with an API key. The cooldown was designed to defend against compromised dashboard sessions; it is not a useful defense against a stolen API key (which already has access to every pre-existing whitelisted address). Dashboard / JWT-session withdrawals keep the cooldown unchanged. Payouts already had this behavior since launch — withdrawals now match. The real defenses for a stolen key remain key rotation, IP allowlist, monitoring, and AML screening on the destination.

added

SDK 1.6.0 — wallets/whitelist + webhook CRUD across all three SDKs

@zyndpay/sdk 1.6.0 (npm), zyndpay 1.6.0 (PyPI), and zyndpay/zyndpay-php 1.6.0 (Packagist) gain 12 new methods each: wallets.{listWhitelist,addToWhitelist,bulkAddToWhitelist,updateWhitelistContexts,removeFromWhitelist} and a new webhookEndpoints resource (create / list / get / update / delete / rotateSecret / reactivate / sendTestEvent). Pair the new wallets methods with the wallets_read/wallets_write scopes; webhook CRUD requires webhooks_read/webhooks_write.

added

fiat-destinations API key access + dedicated scopes

POST/GET/PATCH/DELETE /v1/merchants/fiat-destinations now accept API keys (previously dashboard-only). Use the new fiat_destinations_read / fiat_destinations_write scopes — create a key with the Fiat Destinations scope group under Dashboard → API Keys.

added

Bulk whitelist accepts usageContexts

POST /v1/wallets/whitelist/bulk now accepts an optional usageContexts array applied to every address in the batch (defaults to ["WITHDRAWAL"] for back-compat). Send ["WITHDRAWAL", "PAYOUT"] to make addresses immediately payout-eligible without per-entry PATCHing.

added

wallets API key scopes (wallets_read / wallets_write)

All /v1/wallets/whitelist endpoints now accept API keys with the new wallets_read (list) / wallets_write (mutations) scopes — same DualAuthGuard pattern as payouts and withdrawals. Newly added addresses keep the 24-hour security cooldown before they can receive a withdrawal.

added

PATCH /v1/wallets/whitelist/:id/contexts

Replace the usageContexts of an existing whitelist entry — for example to widen a WITHDRAWAL-only address to also accept PAYOUT — without restarting the 24-hour cooldown.

added

Webhook event catalogue updated

Three additional dispute lifecycle events are now subscribable: dispute.resolved, dispute.rejected, dispute.escalated. The full canonical event list is documented in the Webhooks section of this portal — subscribe only to events listed there to avoid silent no-ops.

fixed

GET /v1/merchant/balances/:currency — clearer validation errors

Requests with an unknown currency code now return a 400 with a structured VALIDATION_ERROR payload, matching the rest of the API. No change for valid currency codes (USDT_TRC20, XOF).

added

/v1/merchant/usage/* readable via API key

GET /v1/merchant/usage/rate-limit and /v1/merchant/usage/limits now accept API keys with the analytics_read scope, so programmatic merchants can monitor their own quotas.

added

Developer portal launched

dev.zyndpay.io is now the canonical home for API docs, SDK guides, webhook signature examples, and the error code catalog. Swagger remains the OpenAPI source of truth and is mirrored into the API reference section.

added

Per-merchant resource usage limits

Endpoints now enforce abuse-protection quotas (issue #198). Exceeding a quota returns RATE_LIMITED with a Retry-After header.

added

Dispute escalation workflow

dispute.opened and dispute.escalated webhook events added (issue #200).

breaking

Circuit breaker on Chainalysis

Upstream compliance calls now fail closed (issue #240). Expect COMPLIANCE_HOLD rather than a silent pass when Chainalysis is degraded.

added

Invoice and receipt generation

GET /v1/invoices/:id/pdf returns a signed, printable invoice (issue #197).

Cette page a-t-elle été utile ?