API documentation
Webhooks
Webhooks allow your application to receive real-time notifications when events occur in your Demarky account.
Event types
| Event | Description |
|---|---|
lead.created | A new lead was submitted |
lead.abandoned | A lead was marked as abandoned |
lead.status.changed | A lead's status was updated |
page.generation.completed | A page generation job completed |
page.deployed | A page was deployed |
product.created | A product was created |
product.updated | A product was updated |
product.deleted | A product was deleted |
Delivery contract
Deliveries are HTTP POST requests to your endpoint URL with a JSON body and these headers:
| Header | Description |
|---|---|
Demarky-Signature | t=<unix_seconds>,v1=<hex digest> — timestamp-bound HMAC signature (see below) |
Demarky-Event-Type | The event type (e.g. lead.created) |
Demarky-Event-Id | Event occurrence ID (stable across subscriptions and retries) |
Demarky-Delivery-Id | Unique ID for this delivery (stable across retries of the same delivery) |
Demarky-Account-Id | Opaque account ID for the seller whose data changed |
User-Agent | Demarky-Webhook/1.0 |
The payload envelope:
{
"id": "2f1c8d3e-6a4b-4c7d-8e9f-0a1b2c3d4e5f",
"delivery_id": "4b8a1f2c-3d5e-4a6b-9c8d-7e0f1a2b3c4d",
"type": "product.updated",
"api_version": "v1",
"occurred_at": "2026-07-25T14:32:18.221Z",
"sequence": 1842,
"livemode": true,
"account_id": "acct_01...",
"application_id": "app_01...",
"installation_id": "ins_01...",
"data": {
"object": { "id": "123e4567-e89b-42d3-a456-426614174000", "version": 7 },
"resource_url": "/v1/accounts/acct_01.../products/123e4567-e89b-42d3-a456-426614174000"
}
}
- Delivery is at-least-once — deduplicate by the payload
id. occurred_atis the business time of the event;tinDemarky-Signatureis the send-attempt time.sequenceincreases per account but may contain gaps; delivery order is not guaranteed.- Payloads are thin and exclude lead PII; use the signed resource URL for authorized follow-up reads.
- Your endpoint must respond within 10 seconds or the attempt counts as failed.
Signature verification
Each delivery is signed with the server-generated endpoint secret. During rotation the header
contains multiple v1= values, and a receiver accepts the delivery when any one matches:
Demarky-Signature: t=1784990123,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
To verify:
- Parse
tand everyv1signature fromDemarky-Signature. - Reject timestamps more than five minutes away from your current time.
- Compute HMAC-SHA256 over
t + "." + raw_body, using the exact request body bytes and your webhook secret. - Compare your digest to
v1using a constant-time comparison.
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret, now = Date.now()) {
const entries = signatureHeader.split(',').map((part) => part.trim().split('=', 2));
const timestampText = entries.find(([name]) => name === 't')?.[1];
const signatures = entries.filter(([name]) => name === 'v1').map(([, value]) => value);
if (!timestampText || !/^\d+$/.test(timestampText) || signatures.length === 0) return false;
const timestamp = Number(timestampText);
const ageSeconds = Math.floor(now / 1000) - timestamp;
if (!Number.isSafeInteger(timestamp) || Math.abs(ageSeconds) > 300) return false;
const signedPayload = Buffer.concat([Buffer.from(timestampText + '.'), rawBody]);
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest();
return signatures.some((signature) => {
if (!/^[a-f0-9]{64}$/i.test(signature)) return false;
const received = Buffer.from(signature, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
// Usage in an Express handler (rawBody must be the unparsed body bytes):
const isValid = verifyWebhook(
rawBody,
request.headers['demarky-signature'],
'whsec_your_webhook_secret'
);
Retry and delivery semantics
- Failed deliveries are retried on a fixed schedule: 1 minute, 5 minutes, 30 minutes, then 2 hours after the previous attempt.
- Network errors, timeouts,
408,409,425,429, and5xxare retryable. - Other
4xxresponses and redirects are terminal;410 Gonedisables the endpoint. - Endpoint management, delivery listing, test sends, manual retry, and secret rotation ship in Phase 3b.
Circuit breaker
Circuit state belongs to the endpoint. Ten consecutive retryable failures, or at least 80% retryable
failures in the latest 20 attempts, opens it. A successful half-open probe closes it. Delivery
failure never deactivates an installation or application.
Best practices
- Respond with
2xxquickly — the webhook dispatch does not expect a response body. - Deduplicate by event
idin your store before processing. - Set up monitoring on delivery failure rates via the deliveries endpoint.
- Store the server-generated webhook
secretsecurely when it is returned at creation.