Webhooks for orders and offline searches
Receive order.ready, order.failed and offline_search.ready events from asicapi, verify the HMAC-SHA256 signature, and handle retries idempotently.
Orders are the asynchronous third tier of asicapi's purchasing model, alongside free lookups and synchronous extract purchases; free lookups, extracts and orders explains where they fit and when the fee is incurred. Document image orders, charges extract orders and offline name searches are asynchronous: ASIC retrieves the pages or runs the batch search after the API has already returned 202 Accepted. Webhooks tell your application when that work finishes so you do not have to poll GET /v1/orders/{id}. asicapi signs every delivery with an HMAC-SHA256 signature, retries failed deliveries for 24 hours, and includes the complete order object in the payload so a single event is enough to act on.
Event types
| Event | Fires when | data |
|---|---|---|
order.ready | An order's status becomes ready (web delivery, PDF available at downloadUrl) or delivered (email or post) | The order object |
order.failed | ASIC could not fulfil the order. data.failure explains why | The order object with status: "failed" |
offline_search.ready | An offline name search has completed and the results have been delivered | The offlineSearch object |
New event types may be added within /v1. Ignore events you do not recognise.
Configuring an endpoint
Add an HTTPS URL in the dashboard, choose the environment (test or live) and the event types you want. asicapi shows the endpoint's signing secret once at creation; store it alongside your API key. Each endpoint has its own secret, and you can have several endpoints per environment.
Your endpoint must respond with any 2xx status within 10 seconds. Do the real work (downloading the PDF, updating your database) after acknowledging, or at least keep it short. Any other status, a timeout or a connection failure counts as a failed delivery and schedules a retry.
Payload
{
"id": "evt_01J8ZKM0N5P9Q3",
"type": "order.ready",
"createdAt": "2026-09-04T01:12:41Z",
"livemode": true,
"data": {
"object": "order",
"id": "ord_01J8ZK6M4R9S2T",
"type": "document_image",
"status": "ready",
"documents": [
{
"object": "document",
"documentNumber": "0E5123456",
"formCode": "484",
"formDescription": "Change to company details",
"subForms": [{ "code": "484E", "description": "Change to members register" }],
"receivedAt": "2024-05-01",
"processedAt": "2024-05-03",
"effectiveAt": "2024-04-30",
"qualifier": null,
"pageCount": 4,
"imaged": true,
"underRequisition": false,
"xbrlAvailable": false,
"xbrlDocumentNumber": null,
"status": null,
"priced": true
}
],
"delivery": { "method": "web", "email": null },
"downloadUrl": "https://api.asicapi.dev/v1/orders/ord_01J8ZK6M4R9S2T/download",
"asicRequestIds": ["000114747"],
"createdAt": "2026-09-04T01:12:08Z",
"readyAt": "2026-09-04T01:12:41Z"
}
}| Field | Type | Description |
|---|---|---|
id | string | Unique event id. The same id is sent on every retry of the same event |
type | string | One of the event types above |
createdAt | string | RFC 3339 timestamp of when the event was created |
livemode | boolean | true for events from live keys, false for the sandbox |
data | object | The full order or offlineSearch object as GET /v1/orders/{id} would return it at the time of the event |
An order.failed payload carries the reason in data.failure:
{
"object": "order",
"id": "ord_01J8ZKN1P6Q0R4",
"type": "document_image",
"status": "failed",
"documents": [],
"delivery": { "method": "web", "email": null },
"downloadUrl": null,
"asicRequestIds": ["000114748"],
"createdAt": "2026-09-04T01:30:02Z",
"readyAt": null,
"failure": {
"code": "document_not_imaged",
"asicCode": "DI52",
"message": "Document 0E5123457 is not available from ASIC's imaging system."
}
}Failed orders are not billed.
Headers
| Header | Example | Purpose |
|---|---|---|
asicapi-signature | t=1756948361,v1=5f8a2c... | Timestamp and HMAC-SHA256 signature. See below |
asicapi-event-id | evt_01J8ZKM0N5P9Q3 | Same as id in the body, for logging before parsing |
asicapi-delivery-attempt | 1 | Attempt number, starting at 1 |
Content-Type | application/json; charset=utf-8 | |
User-Agent | asicapi-webhooks/1 |
Verifying the signature
Every delivery is signed so you can be sure it came from asicapi and was not altered in transit. The asicapi-signature header has the form t=<unix timestamp>,v1=<hex signature>. The signature is the HMAC-SHA256 of the string ${timestamp}.${body} using your endpoint secret, where body is the raw request body exactly as received. Verify it before trusting the payload.
- Read the raw body as bytes. Do not parse and re-serialise it; whitespace differences would break the check.
- Split the header on
,and readtandv1. - Compute the HMAC-SHA256 of
${t}.${body}with your endpoint secret and hex-encode it. - Compare with
v1using a constant-time comparison. - Reject the event if
tis more than five minutes from the current time, which defeats replay of a captured delivery.
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
export function verifyAsicapiSignature(
rawBody: string | Buffer,
signatureHeader: string,
secret: string,
now: number = Math.floor(Date.now() / 1000),
): boolean {
const parts = Object.fromEntries(
signatureHeader.split(',').map((kv) => kv.split('=') as [string, string]),
);
const timestamp = Number(parts.t);
const provided = parts.v1;
if (!Number.isFinite(timestamp) || !provided) return false;
if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) return false;
const payload = `${timestamp}.${typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8')}`;
const expected = createHmac('sha256', secret).update(payload).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(provided, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}A minimal handler with Express, reading the raw body:
import express from 'express';
import { verifyAsicapiSignature } from './verify';
const app = express();
app.post('/webhooks/asicapi', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.header('asicapi-signature') ?? '';
if (!verifyAsicapiSignature(req.body, signature, process.env.ASICAPI_WEBHOOK_SECRET!)) {
return res.status(400).send('invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
if (await alreadyProcessed(event.id)) return res.sendStatus(200);
switch (event.type) {
case 'order.ready':
await enqueueDownload(event.data.id, event.data.downloadUrl);
break;
case 'order.failed':
await markOrderFailed(event.data.id, event.data.failure);
break;
case 'offline_search.ready':
await notifySearchReady(event.data.id);
break;
}
await markProcessed(event.id);
res.sendStatus(200);
});When you rotate an endpoint secret in the dashboard, deliveries carry two signatures for 24 hours (v1=<new>,v1=<old>) so you can switch without dropping events. Accept the event if any v1 value verifies.
Retries
If your endpoint does not return 2xx within 10 seconds, asicapi retries with exponential backoff for up to 24 hours: after roughly 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and then every 6 hours until the window closes. Every retry has the same id and body and a fresh signature with the current timestamp. The dashboard shows each attempt with the response code it received, and lets you resend an event manually after the retry window.
Because ASIC download URLs expire seven days after readyAt, an order whose event could not be delivered for 24 hours still has plenty of time to be fetched. GET /v1/orders remains available as a fallback to reconcile any orders you did not hear about.
Idempotent handling
Deliveries can arrive more than once: a retry after a timeout where your handler actually succeeded, or a manual resend. Store processed event ids and skip any you have already seen. If you would rather key on the order, note that order.ready is only ever sent once per order but a slow handler can still see the same event twice, so idempotency on event.id is the safer choice.
Do not rely on ordering. order.ready for two orders created in sequence can arrive in either order, and after an outage a burst of retries arrives together.
Testing webhooks
Endpoints configured for the test environment receive events from sandbox orders. Create an image order for a document of company 009 136 109 with a test key and the order.ready event arrives within a few seconds. Send X-Sandbox-Simulate: order_failed on the order request to receive order.failed instead. See sandbox.
Related
Sandbox and test keys
Develop against ASIC sample data for free with asicapi test keys, learn how placeholder names and images behave, and simulate errors with valid test ACNs.
ASIC company data concepts explained
Plain-English guides to the ASIC register: companies, ACNs and ABNs, office holders, extracts, documents, charges, schemes, business names and registers, each mapped to asicapi fields.