> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prosperavest.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> API date versions and @ensc/sdk releases.

## 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](/going-live).
