Skip to main content
ENSC tells your backend what happened by sending signed events to an https URL you own. Every conversion and payout outcome reaches you this way, without polling. This page is the receiver guide; the event list is in Conversions.

How it works

  1. You register an endpoint: a public https URL on your backend, per environment (Sandbox, Live), subscribed to the event types you want (["*"] for all). From the dashboard (Webhooks tab) or with POST /v1/webhook-endpoints (ensc.webhookEndpoints.create).
  2. ENSC delivers: one POST per event, JSON body, signed headers.
  3. You verify, acknowledge, then process: check the signature over the raw body, answer 200, and do the work afterwards.
  4. ENSC retries until your endpoint answers 2xx or the attempts run out, so you must handle duplicates.
Nothing here needs a tunnel or a request inspector. Your receiver is part of your backend and runs wherever that runs; register the URL it is reachable at.

Registering an endpoint

An endpoint can be disabled and re-enabled (PATCH with status), changed (url, eventTypes, description) and deleted. A disabled endpoint receives no new events, and retries already scheduled for it stop. With the SDK: webhookEndpoints.list(), get(id), update(id, { ... }), remove(id). No secret is issued when you register. Deliveries are signed with ENSC’s own key (next section), so there is nothing to store or rotate on your side. Scopes: a secret key manages webhooks and reads the event log. A restricted key needs webhooks:read to list and read, webhooks:manage to create, change, test or delete endpoints.

The delivery

X-ENSC-Webhook-Id is unique per delivery attempt and is part of the signed string.

Verifying a delivery

The signature is Ed25519 over
made with the ENSC key named by X-ENSC-Key-Id. The public keys are published at GET /v1/.well-known/ensc-public-keys.json (entries with use containing webhooks); EnscClient.fetchPublicKeys() loads them as { [kid]: publicKey }. Fetch the document once, cache it per key id, and refetch on an unknown id: that is how a key rotation reaches you without any action on your side. Rules that keep the check sound:
  • Hash the raw bytes you received. Parsing the JSON and re-serialising it changes the bytes and the signature no longer matches.
  • Refuse a delivery whose timestamp is more than 5 minutes from your clock; a retry always carries a fresh timestamp and signature.
  • Compare in constant time, or use the SDK.
With @ensc/sdk:
constructEvent takes body as a string or bytes, exactly as received and before any JSON parsing, headers as a Headers instance or a plain record (Node’s req.headers works), and publicKey, the key map from EnscClient.fetchPublicKeys() (a single key string is accepted too). The verifier picks the key named by X-ENSC-Key-Id, so a key rotation needs no redeploy. It throws ENSC_INVALID_SIGNATURE when the signature does not verify under that key, the key id is not in the map you pass, or the timestamp is outside the window, and ENSC_VALIDATION_FAILED when the verified body is not an event envelope. EnscClient.verifyWebhookSignature is the non-throwing form and returns { valid, reason? }; reason is unknown_key_id when the key id is not in your map, which is your cue to refetch the keys. Any Ed25519 implementation can do the same check without the SDK; the SDK’s test suite carries the vectors.

Answering and processing

  • Answer 2xx within 15 seconds. ENSC waits that long. Verify, record the event id, answer, then process. Do not call your bank, ENSC or a chain node before answering.
  • Any other outcome is retried: a non-2xx answer, a redirect, a timeout or a connection failure. The retries come 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 the delivery is marked gave_up. Disabling the endpoint ends its retries.
  • Delivery is at least once. A retry after a lost answer delivers the same event id again. Store the ids you have processed and skip duplicates.
  • Order is not guaranteed. Two events for one conversion can arrive out of order or seconds apart. Act on the status the event carries, not on the sequence, and before releasing goods or money read the conversion back with GET /v1/conversions/{reference} (ensc.conversions.get(reference)).
  • Credit on the final event: conversion.succeeded for crypto legs and fiat-issue, payout.succeeded for a fiat-redeem. Earlier events are progress, not money.
  • Ignore fields you do not know. New fields can appear in a payload; write the handler so they do not break it.

Seeing what was delivered

GET /v1/events (ensc.events.list) is the log of every event ENSC generated for you, with env and type filters. GET /v1/events/{id} (ensc.events.get) adds the delivery attempts: for each, the endpoint, the attempt number, status (pending, delivered, failed, gave_up), the HTTP status your endpoint answered, when the next attempt is due and the first 500 characters of your response. The dashboard shows the same log.

Testing your receiver

  1. Deploy the receiver to the URL you will register, on the same hosting as the rest of your backend.
  2. Register that URL in Sandbox.
  3. POST /v1/webhook-endpoints/{id}/test (ensc.webhookEndpoints.sendTest(id, { eventType })) queues one signed event in that endpoint’s environment; every active endpoint there that subscribes to the type receives it, and the response says whether the endpoint you named is among them (willDeliverToTargetEndpoint). With an eventType from the catalogue and no payload, the delivery carries the fields a real event of that type carries; with no eventType it is a synthetic.test_event. A Live endpoint accepts only synthetic.test_event (ENSC_TEST_LIVE_MISMATCH otherwise), so a real event type can never be forged into a Live receiver. POST /v1/test-data/events (ensc.testEvents.emit) sends a realistic event to every Sandbox endpoint, and GET /v1/test-data/events (ensc.testEvents.list) shows the catalogue with a sample payload per type.
  4. Read the attempt back with GET /v1/events/{eventId} and confirm status: "delivered" with your 200.
During local development, before the receiver is deployed, any tool that gives a local port a public https hostname works for step 2. It is a stand-in for the deployed URL, not part of the integration, and the URL changes when the tool restarts.

Going live

Register a Live endpoint (a Live key or the Live dashboard) with your production URL. Sandbox endpoints never receive Live events and Live endpoints never receive Sandbox events, so both can stay registered. Keep the same verification code: the key set and the signing scheme are the same in both environments.