DutyClaims Docs
Webhooks

Register endpoints early and verify signatures before you trust event traffic.

DutyClaims exposes webhook registration, delivery inspection, and synthetic sandbox test traffic. This page gives you the operational setup path that sits between the quickstart and the raw reference.

Practical setup order

  1. Create the endpoint with a label, URL, subscribed events, and the correct environment scope.
  2. Persist the returned signing secret immediately. The create response is where you receive it.
  3. Implement signature verification before you process event payloads.
  4. Use synthetic sandbox webhook tests before you register production endpoints.
  5. Inspect delivery logs when debugging retries, not just the receiving app logs.

Current webhook-related routes

  • POST /v1/sandbox/webhooks/test Trigger a synthetic sandbox webhook event
  • GET /v1/webhook-deliveries List webhook deliveries for the authenticated partner
  • GET /v1/webhooks/endpoints List webhook endpoints for the authenticated partner
  • POST /v1/webhooks/endpoints Register a webhook endpoint

Register an endpoint

curl -X POST https://api.dutyclaims.com/v1/webhooks/endpoints \
  -H "Authorization: Bearer dcp_live_replace_me" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "partner-production",
  "url": "https://partner.example.com/webhooks/dutyclaims",
  "subscribedEvents": ["claim.updated", "partner.credential.rotated"],
  "environmentScope": "production"
}'

The create response returns both the endpoint record and the signing secret. Treat that secret as write-only setup material and store it outside your application source.

Verify signatures

import { createHmac, timingSafeEqual } from "node:crypto"

const secret = process.env["DUTYCLAIMS_WEBHOOK_SECRET"] ?? "dcp_test_webhook_secret"

export function verifyDutyClaimsWebhook(input: {
  payload: string
  headers: Headers
}): boolean {
  const timestamp = input.headers.get("X-DutyClaims-Timestamp")
  const signature = input.headers.get("X-DutyClaims-Signature")

  if (!timestamp || !signature) return false

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${input.payload}`)
    .digest("hex")

  return (
    signature.length === expected.length &&
    timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  )
}

export function readDutyClaimsEvent(input: { headers: Headers }): string | null {
  return input.headers.get("X-DutyClaims-Event")
}

The generated example verifies X-DutyClaims-Timestamp and X-DutyClaims-Signature, then exposes the event name through X-DutyClaims-Event.

Current delivery headers

HeaderMeaningVerification rule
X-DutyClaims-EventThe canonical event name for this delivery.Route on this value only after signature verification succeeds.
X-DutyClaims-TimestampThe signing timestamp used to build the HMAC base string.Use the exact header value when recreating the expected signature.
X-DutyClaims-SignatureThe hex-encoded HMAC-SHA256 signature for the payload JSON.Compare it using a timing-safe check against your locally computed HMAC of `timestamp.payload`.

Signed payload rules

The current shared signing model uses the exact string ${timestamp}.${payloadJson} as the HMAC base, then signs it with SHA-256. Preserve the raw request body bytes before parsing JSON so your verification step uses the same payload string DutyClaims signed.

  • Do not parse and re-stringify the payload before verifying the signature.
  • Reject deliveries with missing timestamp or signature headers before routing on the event name.
  • Use a timing-safe comparison for the computed and supplied signatures.
  • The current contract does not publish a built-in replay-window tolerance, so enforce your own timestamp freshness policy on the receiver.

What the registration response gives you

  • signingSecret: the one-time secret you must store immediately for later verification.
  • endpoint.id: the durable UUID you use when filtering delivery history or targeting sandbox tests.
  • endpoint.secretPrefix: a safe identifier for matching stored config without exposing the secret itself.
  • endpoint.status: current endpoint lifecycle state, published as active, disabled, revoked.
  • endpoint timestamps: delivery, success, and failure timestamps tell you whether the listener ever worked in the intended environment.

Published event vocabulary

  • Current published event types: partner.test, partner.capability.changed, partner.environment.promoted, partner.credential.rotated
  • Subscription semantics: register the exact event names your endpoint expects, and keep environment scope aligned with where the downstream system actually runs.
  • Sandbox testing: the synthetic test route publishes the same typed event vocabulary and returns a signed preview so you can debug without waiting on live lifecycle changes.

Synthetic sandbox tests

curl -X POST https://api.dutyclaims.com/v1/sandbox/webhooks/test \
  -H "Authorization: Bearer dcp_test_replace_me" \
  -H "Content-Type: application/json" \
  -d '{
  "eventType": "partner.test",
  "environment": "sandbox"
}'
  • Current synthetic event types: partner.test, partner.capability.changed, partner.environment.promoted, partner.credential.rotated
  • Endpoint environment scopes: sandbox, production, all

Delivery state model

  • Endpoint statuses: active, disabled, revoked
  • Delivery statuses: queued, delivered, retrying, failed
  • Delivery debug fields: use attemptCount, lastResponseStatus, lastError, lastAttemptAt, nextAttemptAt, and deliveredAt before guessing from app logs.
  • Use webhook deliveries to see attempt counts, last response status, failure detail, and next retry timing.
  • Do not guess from application logs alone when the API already exposes delivery history for the partner tenant.

Reference deep links