Skip to main content

API

17 September 2026

Changes within date version 2026-09-15; the header value is unchanged and no client change is required. Conversions
  • from on unsigned transactions. Every piece of calldata ENSC returns (approvalTransaction, transaction on a voucher, and unsignedTransaction on a transfer) now names the wallet that must sign it: { from, to, data, value: "0", chainId }.
  • Create finishes a created conversion on re-post. If a create was cut off between the insert and the voucher (a timeout, a signer or bank-rail error), the conversion is left at created with lastError recorded. Re-posting the same reference now finishes it (HTTP 200) instead of returning the stuck row; POST /v1/conversions/{reference}/voucher does the same.
  • Reporting failed is limited. A conversion can be reported failed only before a transaction is recorded for it, and not while it is in requires_manual_review; otherwise the answer is 409 ENSC_INVALID_STATE.
  • Confirmation depth. A receipt must be as many blocks deep as the chain’s confirmation depth requires before onchain_confirmed settles. Until then, confirmed answers 409 ENSC_SETTLEMENT_VERIFICATION_FAILED with details.reason not_enough_confirmations and details.retriable true.
  • Unexpected Naira payments go to review. A fiat-issue payment in a currency other than NGN, or above the amount, now parks the conversion in requires_manual_review, as a short payment already did.
  • Payouts are sent once. Concurrent calls (your retry, a scheduled retry) can no longer send the same payout twice, and a payout’s status is updated only from its own bank transfer’s notifications.
  • Status changes cannot overwrite each other. A conversion moves to a new status only if it is still in the status the change was based on.
  • Neutral error text. lastError and webhook payloads carry a neutral message instead of the bank rail’s raw error text.
  • Amount bounds. An amount beyond the supported range is refused with a 400 instead of failing with a 500.
Webhooks
  • Retries run as documented. A delivery that fails (a non-2xx answer, a redirect, a timeout or a connection failure) is retried after 1 minute, 5 minutes, 15 minutes, 1 hour, 2 hours, 4 hours and 8 hours: 8 attempts in total over about 15 hours, then gave_up. Redirects are never followed. Disabling an endpoint ends its retries.
  • Test deliveries carry realistic payloads. POST /v1/webhook-endpoints/{id}/test with an eventType from the catalogue and no payload, and POST /v1/test-data/events, deliver the fields a real event of that type carries. GET /v1/test-data/events lists the catalogue with a sample payload per type.
  • Live endpoints accept only synthetic.test_event. A test delivery of any other type to a Live endpoint is refused with ENSC_TEST_LIVE_MISMATCH, so a real event type can never be forged into a Live receiver.
  • Webhook scopes for restricted keys. A restricted key needs webhooks:read to list endpoints and read events, and webhooks:manage to create, change, test or delete endpoints. A secret key manages webhooks and reads the event log without them.
  • The event log is environment-scoped. An API key reads only the events of its own environment.
Keys and requests
  • Idempotency keys must be 8 to 64 characters of A-Z a-z 0-9 _ - (400 ENSC_VALIDATION_FAILED otherwise), are scoped to your account and environment, and are honoured on every write route, including webhook endpoints, test events and credential writes.
  • Request body limit. A request body may be at most 1 MiB.
  • Write rate limit counts writes only. The 60-per-minute limit on the conversion, account-resolution and transfer routes no longer counts reads.
  • Publishable keys are refused on every management route (credential lists, webhook endpoints, events and test data).
  • Allowlists and origins. An IP allowlist CIDR entry must have a prefix of at least /8 (IPv4) or /32 (IPv6), and over-broad CORS origin patterns are refused when you register them.
Reference and documentation
  • The public reference. https://api.ensc.prosperavest.com/v1/openapi.json and https://api.ensc.prosperavest.com/v1/docs no longer list the dashboard-only credential writes. Every operation shows its @ensc/sdk call, and the create, test delivery and test event operations document their 202 answers. The document also carries notes on amounts, unsigned transactions and webhooks, and documents the Idempotency-Key alias.
  • Documentation corrections. The webhook retry schedule (8 attempts over about 15 hours, not 40 hours), test deliveries (queued in the endpoint’s environment, not sent to one endpoint only), metadata (echoed on the conversion object, not in webhooks), the error codes for an unknown or retired signing key id, and the wording of the canonical request string.
  • Chains. Arbitrum One (arbitrum), BNB Smart Chain (bsc) and Mode (mode), with their testnets, join the registry as token-only chains; each is served once it is enabled for an environment.
  • SDK. @ensc/sdk 0.4.1 carries the matching client fixes; see below.

2026-09-15

The current date version. Sent as X-ENSC-API-Version: 2026-09-15; @ensc/sdk 0.3.0 and later pin it.
  • Mandatory request encryption (ENSC-ENC-V1). Every write body is an AES-256-GCM envelope { v, encKeyId, iv, ciphertext, tag } bound to the method, path, merchant id and key id. Plaintext merchant writes are refused with ENSC_ENCRYPTION_REQUIRED.
  • Sealed responses (ENSC-RESP-V1). Every successful response is sealed to the merchant’s signing key with HPKE (RFC 9180 base mode: X25519, HKDF-SHA256, ChaCha20-Poly1305) and signed by ENSC. Errors are never sealed.
  • Public key discovery. ENSC’s Ed25519 public keys are published at GET /v1/.well-known/ensc-public-keys.json, for responses and webhooks.
  • X-ENSC-Key-Id on reads. Sent on reads as well as writes, so the API knows which registered key to seal the response to.
  • Conversions. The API moved from the direct mint and redeem model to the ENSC converter: POST /v1/conversions with four types (crypto-issue, crypto-redeem, fiat-issue, fiat-redeem), GET /v1/conversions, GET /v1/conversions/{reference}, GET /v1/conversions/quote, GET /v1/conversions/screening, and POST /v1/conversions/{reference}/events, /voucher and /payout. Bank directory and account resolution: GET /v1/banks, POST /v1/accounts/resolve. Conversions run on celo (Live) and celo-sepolia (Sandbox).
  • Credential writes removed from the API. Issuing, rotating and revoking keys, and editing allowlists or origins, are dashboard actions (ENSC_DASHBOARD_ONLY); the list operations remain.
  • Removed routes. The previous mint, redeem, approve, withdraw, verify-payout, virtual-account and mint-limit routes no longer exist; clients built for them receive ENSC_NOT_FOUND.

@ensc/sdk

0.4.1

Requires API date version 2026-09-15 (unchanged). Fixed
  • @ensc/sdk/web3: signAndBroadcast estimates gas first (without fee fields) and sends with an explicit limit (estimate plus 30 percent, or the gas you pass). Without a limit some nodes estimate at the block gas limit and charge that gas up front during the simulation; on Celo that empties the CELO balance the converter then pulls from, and a crypto-issue with pair: 'CELO' failed with transfer value exceeded balance of sender from any wallet holding less than a few CELO. A failed simulation now raises ENSC_UPSTREAM_FAILED with the node’s reason and broadcasts nothing.
  • @ensc/sdk/web3: the helper refuses a signer that is not the from of the transaction, an RPC endpoint on another chain, and a voucher whose wallet is not its transaction’s signer.
  • EventDelivery now carries every field the API returns: endpointId, responseStatus, deliveredAt, nextAttemptAt, responseSnippet; the status union is documented. EventDetail gains apiVersion and dispatchedAt.
  • webhookEndpoints.sendTest is typed (SendTestEventResponse), and its doc comment no longer mentions a per-endpoint secret; deliveries are signed with ENSC’s published Ed25519 key.
  • Every EnscError now carries status (the HTTP status received, so a code added by the API later still classifies correctly) and requestId (from the error body or X-ENSC-Request-Id).
  • An empty 2xx body is refused as ENSC_INVALID_SIGNATURE (reason: 'empty_body'): no route this client calls answers without a sealed envelope, so a stripped body is treated like an unsigned one.
  • A polyfilled fetch that reports the timeout as AbortError is now recognised as a timeout; a body that cannot be read is retried like a network error.
  • Webhook verification: the timestamp is signed as the exact header string (digits only), a byte body is hashed as received, a repeated header in a Node IncomingHttpHeaders record is refused instead of thrown on, and a verified body that is not an event envelope is refused by constructEvent (ENSC_VALIDATION_FAILED).
  • config.baseUrl must be https (http is allowed for localhost only).
  • Type drift: WebhookEndpoint and CreateWebhookEndpointResponse gain apiVersion; EventSummary gains apiVersion and dispatchedAt and its status is the documented EventOutboxStatus (created, in_flight, dispatched, abandoned); SendTestEventResponse gains deliveredVia; GetBalanceParams requires asset or contractAddress.
  • CommonJS consumers get matching .d.cts declarations (exports now names types per condition).
  • README: the sentence about what ENSC retains was wrong; corrected. New sections on unsigned transactions, amounts and decimals, and a complete webhooks guide.
Added
  • Signer: signAndBroadcast and executeVoucher accept a raw private key or any viem account (local, custody or KMS via toAccount, or the JSON-RPC account of a connected wallet). A reverted converter call now throws ENSC_UPSTREAM_FAILED with the hash (details.txHash) instead of returning a result to report as confirmed; receipt errors are wrapped the same way.
  • EnscClient.fetchPublicKeys() (also exported as fetchEnscPublicKeys): loads ENSC’s webhook keys as { [kid]: publicKey }. verifyWebhookSignature and constructEvent accept that map and pick the key named by X-ENSC-Key-Id, so a key rotation needs no redeploy.
  • ListByEnvParams: apiKeys, signingKeys, encryptionKeys and origins list() accept an env filter, as the API does.
  • testEvents resource: list() (GET /v1/test-data/events, the catalogue with a sample payload per type) and emit({ eventType, overrides }) (POST /v1/test-data/events, with a Sandbox or Live key; the events are always Sandbox events and reach only Sandbox endpoints).
  • UnsignedTransaction.from: every piece of calldata now names the wallet that must sign it (API change of 17 September 2026).
  • BroadcastOptions.gas and BroadcastOptions.gasMarginPercent.

0.4.0

Breaking: the API moved from the direct mint / DEX redeem model to the ENSC converter. Requires API date version 2026-09-15. 0.3.x clients call routes that no longer exist and receive ENSC_NOT_FOUND. Added
  • conversions: create, get, list, quote, screening, voucher, payout, and conversions.events.{submitted, confirmed, failed}. Four conversion types (crypto-issue, crypto-redeem, fiat-issue, fiat-redeem); create returns a signed voucher with the calldata the merchant wallet signs (approvalTransaction, transaction), or bank-transfer paymentInstructions for a fiat-issue.
  • banks.list() and accounts.resolve() (bank directory and account name enquiry for the key’s environment).
  • CONVERTER_CHAINS, ASSETS, PAIRS, Asset, Pair exports; celo and celo-sepolia in KNOWN_CHAINS.
  • @ensc/sdk/web3: executeVoucher(issuedVoucher, signer, { rpcUrl }) (in 0.4.0 the signer was a raw private key) sends the approval then the converter call and returns both hashes; signAndBroadcast now takes { to, data, value, chainId }, fills gas, fees and nonce from the RPC, and returns { txHash, status?, blockNumber? } (waitForReceipt: false returns as soon as the hash is known).
  • Conversion types re-exported: Conversion, ConversionStatus, IssuedVoucher, PaymentInstructions, QuoteResponse, and the create parameter unions.
Changed
  • License changed from MIT to Apache-2.0 (see LICENSE and NOTICE). Earlier versions remain under MIT.
  • balance.get accepts asset: 'ENSC' | 'USDC' | 'USDT' | 'CELO'.
  • transfer.create sends asset: 'ENSC' and returns { unsignedTransaction: { to, data, value: '0', chainId }, chain, amount }.
  • Unsigned transactions no longer carry gasLimit, fees or nonce; the merchant’s signer fills them.
  • Dependencies: zod 4 (was 3), @noble/curves, @noble/hashes and @noble/ciphers 2 (were 1). The api schema types are zod 4 types; code that combines them with its own zod 3 schemas has to move to zod 4 as well.
  • The code the SDK bundles now comes from two packages published as source in the public SDK repository: @ensc/protocol (signing, request envelope, sealed responses, errors) and @ensc/api-schemas (API types). The public exports of @ensc/sdk are unchanged. The build puts code shared by @ensc/sdk and @ensc/sdk/web3 in one chunk in both module formats.
  • Requires Node 20.19+ or 22.12+ (was 20+). The @noble 2 packages are ESM only; the CommonJS build loads them through require() of ES modules, which Node supports from those versions (Node 21 does not).
Removed
  • mint, redeem, approve, withdraw, verifyPayout, virtualAccounts, mintLimit resources and their parameter types.
  • signTransaction from @ensc/sdk/web3 (offline signing needs gas and nonce, which the API no longer supplies).
  • The chain slugs and asset of the previously supported network, which is no longer served.
Earlier releases (0.3.0 and before) target API contracts that no longer exist; upgrade to 0.4.0 or later. See Going live.