SpaceMediaSpaceMedia
Integrations

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

EventScopeMeaning
ddex.delivery.status_changedDDEX messageControl, transport, or partner processing state changed.
ddex.delivery.blockedDDEX messageValidation, review, contract, or state checks blocked progress.
ddex.delivery.acknowledgedDDEX messageThe partner acknowledged the message.
ddex.delivery.rejectedDDEX messageThe partner rejected the message.
ddex.delivery.failedDDEX messageRetryable transport work exhausted its allowance or a non-retryable transport block occurred.
ddex.import.completedDDEX import and source messageAn atomic create-only import completed.
ddex.import.failedDDEX import and source messageAn import failed without partial catalog creation.
ddex.connection.degradedDDEX connectionConnection 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-separated v1= 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 2xx response 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

  1. Verify timestamp and signature against the raw body.
  2. Deduplicate by SpaceMedia-Webhook-Id.
  3. Persist the event and return a success response.
  4. Fetch the canonical resource URL with your bearer token.
  5. Apply the current resource state idempotently.
  6. 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?

On this page