Webhooks
Enterprise webhooks are available initially for DDEX operations. They use HTTPS-only endpoints, HMAC-SHA256 signatures, at-least-once delivery, bounded retries, test events, delivery logs, and reveal-once signing secrets.
Current availability
Enterprise webhook routes follow DDEX availability. They are currently available in staging for receiver testing and conformance. Do not depend on production delivery until SpaceMedia confirms that the production routes and your DDEX connection are active.
Webhook bodies stay compact. Fetch the canonical resource URL in each event before making an irreversible decision.
Available events
| Event | Scope | Meaning |
|---|---|---|
ddex.delivery.status_changed | DDEX message | Control, transport, or partner processing state changed. |
ddex.delivery.blocked | DDEX message | Validation, review, contract, or state checks blocked progress. |
ddex.delivery.acknowledged | DDEX message | The partner acknowledged the message. |
ddex.delivery.rejected | DDEX message | The partner rejected the message. |
ddex.delivery.failed | DDEX message | Retryable transport work exhausted its allowance or a non-retryable transport block occurred. |
ddex.import.completed | DDEX import and source message | An atomic create-only import completed. |
ddex.import.failed | DDEX import and source message | An import failed without partial catalog creation. |
ddex.connection.degraded | DDEX connection | Connection health requires operator attention. |
Event payload
Every event carries a stable event name, a public resource reference, a canonical API path, and an occurrence time. Fetch the canonical path to obtain current state because the resource can change after the event was created.
{
"event": "ddex.delivery.status_changed",
"resource": {
"type": "ddex_message",
"reference": "45be9e79-f39e-4cf9-bfc8-bf9f10481da6",
"url": "/api/v1/ddex/messages/45be9e79-f39e-4cf9-bfc8-bf9f10481da6"
},
"occurred_at": "2026-07-30T15:30:00Z"
}A replay adds replay_of with the original delivery reference and receives a new SpaceMedia-Webhook-Id. The test endpoint sends enterprise.webhook.test with the webhook subscription as its resource. Test events do not need to appear in the subscription's event filter.
Create a subscription
POST /api/v1/webhooks requires webhooks:manage and an Idempotency-Key.
{
"name": "Catalog operations",
"endpoint_url": "https://hooks.northstar-distribution.co.uk/spacemedia",
"events": [
"ddex.delivery.status_changed",
"ddex.delivery.blocked",
"ddex.delivery.acknowledged",
"ddex.delivery.rejected"
]
}The signing secret is returned once. Store it in a secrets manager. Later reads return only secret_configured and the rotation timestamp.
Verify a delivery
The request includes:
SpaceMedia-Webhook-Id: stable delivery reference for deduplication.SpaceMedia-Webhook-Timestamp: Unix timestamp.SpaceMedia-Webhook-Signature: one or more comma-separatedv1=HMAC values. Two values are sent during the five-minute secret-rotation overlap.
Compute HMAC-SHA256 over:
timestamp + "." + exact_raw_body
Reject the request when the timestamp is more than five minutes away from your server clock or the signature does not match by constant-time comparison. Synchronize receiver clocks with a reliable time source.
TypeScript:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(rawBody: string, timestamp: string, header: string, secrets: string[]) {
const received = header.split(",").map((value) => value.trim().replace(/^v1=/, ""));
return secrets.some((secret) => {
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
return received.some(
(value) => expected.length === value.length && timingSafeEqual(Buffer.from(expected), Buffer.from(value)),
);
});
}PHP:
$received = array_map(
fn ($value) => preg_replace('/^v1=/', '', trim($value)),
explode(',', $signatureHeader),
);
$valid = false;
foreach ($activeSecrets as $secret) {
$expected = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);
$valid = $valid || collect($received)->contains(
fn ($value) => is_string($value) && hash_equals($expected, $value)
);
}
if (! $valid) {
http_response_code(401);
exit;
}Delivery contract
- Delivery is at least once. Store the webhook delivery reference and process duplicates safely.
- Event ordering is not guaranteed across retries or subscriptions.
- Any
2xxresponse marks the attempt as delivered. Other statuses are not successful acknowledgements. - Return a successful response after durable acceptance, then process slow work asynchronously.
- Redirects are not followed.
- Endpoints must use HTTPS and resolve only to public network addresses.
- Payloads are limited to 256 KB.
- Connection establishment has a five-second limit and a delivery attempt has a ten-second default limit.
- A timeout, connection failure, throttling response, or temporary server response can be retried.
- Retry delays are 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 2 hours, and 6 hours after the initial attempt.
- A failed subscription is not disabled automatically. Pause it explicitly while repairing the receiver.
- Inspect attempts through
GET /api/v1/webhooks/{reference}/deliveries.
Manage, test, and rotate
Use PATCH /api/v1/webhooks/{reference} to update the name, HTTPS endpoint, event set, or active state. Set active to false to pause delivery while retaining delivery history. Use DELETE /api/v1/webhooks/{reference} only when the subscription and its delivery history can be permanently removed. Both mutations require an Idempotency-Key.
Use POST /api/v1/webhooks/{reference}/test before depending on event delivery. The test passes through the normal signing, queue, delivery, and log path.
Use POST /api/v1/webhooks/{reference}/rotate-secret to replace the secret. The new value is returned once. For five minutes, deliveries include signatures made with both the new and previous secrets. Configure the receiver with both secrets during that overlap, then remove the old secret after previous_secret_valid_until. Never copy either secret into source control, logs, tickets, or support messages.
Use POST /api/v1/webhooks/{reference}/deliveries/{deliveryReference}/replay for an operator-controlled replay. A replay receives a new delivery reference and retains replay_of for audit and deduplication.
Consumer workflow
- Verify timestamp and signature against the raw body.
- Deduplicate by
SpaceMedia-Webhook-Id. - Persist the event and return a success response.
- Fetch the canonical resource URL with your bearer token.
- Apply the current resource state idempotently.
- Keep periodic reconciliation until event delivery is proven for the connection.
Support escalation data
Provide the subscription reference, delivery reference, event name, timestamp with timezone, HTTP status, and DDEX correlation key. Never send signing secrets, bearer tokens, private keys, or raw credentials.
Common questions
Should I trust the payload without verifying the signature? Never. Signature verification is the only thing separating a real event from anyone who learned your endpoint URL.
How do I rotate a secret without dropping events? Accept both the old and new secret during the overlap window, then retire the old one once you have seen traffic verified against the new one.
Do I need to be idempotent? Yes. Retries and replays mean the same event can arrive more than once. Key your processing on the event reference.
My consumer was down. Are those events lost? Use the delivery attempt and replay tooling to recover rather than reconstructing state by hand.
Was this page helpful?