Webhook signatures
When you monitor identifiers in the console, DID.is sends an HTTPS request to your endpoint when something changes, for example a DID document or its domain binding. Each request is signed with your webhook secret, so your receiver can tell it really came from DID.is for your project.
Here your receiver does the checking, not DID.is. Verify every request before you act on it. The SDK helpers do this in one call.
- HMAC-SHA256 over the raw body
- Timestamp is not in the future or too old
- specVersion, event ID, tenant and project
- Event ID not processed before
What a valid signature shows
Section titled “What a valid signature shows”- Origin. The body was signed with your project’s webhook secret, so it came from DID.is (or from someone who has your secret).
- Integrity. The body was not changed in transit.
- Scope. The event belongs to the tenant and project you expect.
What it does not show
Section titled “What it does not show”- Anything more about the identifier than the event itself says. A
document.changedevent says the document changed, not whether the change is legitimate. - That the event is new. Retries resend the same event. You must remove duplicates yourself.
- A paid entitlement. A valid signature is not a billing grant.
Headers
Section titled “Headers”| Header | Value |
|---|---|
X-DIDIS-Event-ID |
The event ID, cevent_…. Same on every retry. |
X-DIDIS-Signature |
t=<createdAt>,v1=<hex HMAC-SHA256> |
The signed bytes are <t>.<raw body>: the timestamp, a period, then the exact request body. Verify against the raw body before parsing it; re-serialized JSON will not match.
Verify with the SDK
Section titled “Verify with the SDK”The helpers check the HMAC-SHA256 signature, the timestamp (not more than 60 seconds in the future, not older than 30 days by default), specVersion (didis.customer-monitor.v1), that the event’s createdAt equals the signed timestamp, and that the event’s id, tenantId and projectId match what you expect. They return true or false.
import { verifyCustomerMonitorSignature } from "@didis/client";
// Express example: use the raw body.app.post("/didis/webhook", express.text({ type: "*/*" }), async (req, res) => { const ok = await verifyCustomerMonitorSignature( process.env.WEBHOOK_SECRET!, req.body, // raw string req.get("X-DIDIS-Signature") ?? "", { tenantId: process.env.TENANT_ID!, projectId: process.env.PROJECT_ID!, eventId: req.get("X-DIDIS-Event-ID") ?? "", }, ); if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body); if (await alreadyProcessed(event.id)) return res.sendStatus(200); await recordAndHandle(event); res.sendStatus(200);});from didis import verify_customer_monitor_signature
ok = verify_customer_monitor_signature( secret, raw_body, # bytes headers["X-DIDIS-Signature"], tenant_id=tenant_id, project_id=project_id, event_id=headers["X-DIDIS-Event-ID"],)if not ok: return 401# Compute the expected v1 value for a saved body, to compare by hand.T=1791678083 # the t= value from X-DIDIS-Signatureprintf '%s.%s' "$T" "$(cat body.json)" \ | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hexBoth helpers take an optional maximum age in seconds (maxAgeSeconds / max_age_seconds), from 1 second up to the 30-day default. Your tenant and project IDs are shown in the console. Keep the secret server-side.
The shell example is for debugging only. $(cat …) drops a trailing newline, so it matches only bodies without one; in code, always use the exact raw bytes.
Reading the result
Section titled “Reading the result”| Situation | What it means | What to do |
|---|---|---|
Helper returns true, event ID is new |
A genuine event for your project. | Record the event ID, then act, then answer 2xx. |
Helper returns true, event ID seen before |
A retry of an event you already have. | Answer 2xx and do nothing else. |
Helper returns false |
Wrong secret, changed body, too old, or not your tenant or project. | Answer 401 and ignore the body. |
Retries and duplicates
Section titled “Retries and duplicates”- A delivery fails if your endpoint does not answer 2xx within 10 seconds. DID.is retries up to five attempts in total, after 1, 2, 4 and 8 minutes.
- Retries send the identical event with the same ID and signature timestamp.
- Record the event ID before side effects, and answer duplicates with 2xx.
- Receivers must use HTTPS on port 443 at a public address. Redirects are not followed; a 3xx answer counts as a failure.
Next steps
Section titled “Next steps”See Monitoring and webhooks for setting up monitoring, the event types and the payload.