Verifying webhooks
Keep the raw request bytes until verification finishes. A webhook is an incoming request from Anis, with its own signature profile. Its signature is not the request-bound answer profile.
The signed components
Use RFC 9421, label sig1, algorithm ecdsa-p256-sha256, and tag anis-partner-webhook-v1. Require these covered components in this exact order:
@method @target-uri content-digest webhook-id anis-webhook-attempt anis-application-id
The following header example illustrates the profile; the signature placeholder is not a test vector.
Signature-Input: sig1=("@method" "@target-uri" "content-digest" "webhook-id" "anis-webhook-attempt" "anis-application-id");created=1792483200;expires=1792483500;keyid="partner-webhook-signing/0123456789abcdef0123456789abcdef";alg="ecdsa-p256-sha256";tag="anis-partner-webhook-v1"
Signature: sig1=:<base64 of the 64-byte P1363 signature>:
@method is uppercase POST. @target-uri is your configured full HTTPS URL, including its query if present. Header names are case insensitive; component identifiers in the signature are lower case. Content-Digest is sha-256=:<standard base64 SHA-256 of the exact body bytes>:. Base lines are UTF-8 joined by LF with no final LF. The signature is the fixed 64-byte P1363 r-then-s form, standard base64, not DER or base64url.
Verify before acting
- Require both Signature-Input and Signature, exactly one signature label, the exact algorithm, tag and ordered component list.
- Compute the digest from the received raw body, before JSON parsing or reserialization, and require it to match Content-Digest.
- Require the configured full target URL, your application id and POST method.
- Check Unix-second times against a synchronized clock:
expires-created == 300,abs(now-created) <= 300, andnow <= expires. There is no extra grace after expiry. Each delivery attempt is freshly signed. - Fetch
/.well-known/partner-signing-keys.jsonover HTTPS from your Anis deployment. Match the exact opaque kid, requirepurpose: "webhook", status active or next, andnotBefore <= created. Reject retired or compromised keys and any matching revoked entry. Refuse a key document containing private key material. - Rebuild the signature base from the actual request and parameters, and verify the 64-byte signature with that public key. Do not trust a supplied component list without comparing it to the frozen list.
- Only then parse the envelope, require id to equal webhook-id and applicationId to equal anis-application-id, handle the verified notification and deduplicate its event id.
The same key document also publishes answer keys with purpose: "answer". A key’s presence alone does not authorize it to verify webhooks. The webhook key is separate from the answer-signing key. Do not apply the answer profile’s @status, ;req or 60-second freshness rule to incoming webhooks.
Accept a delivery once
After verification, durably record the event and process it once. Return a 2xx within ten seconds. Repeated verified deliveries of the same event id should also receive 2xx. Do not deduplicate unverified requests, which could otherwise suppress a genuine event.
A verification notification uses the same signature checks; echo only the verified challenge, as described in Webhooks. A test notification also uses this profile. Neither creates a purchase.
SDK support and test examples
SDK 2.2.0 verifies webhooks in .NET, Node.js, PHP and Python; SDK 2.1.0 does not. Follow this profile and the published signing test vectors. API answer verification remains a separate check.