Create a subscription
CallPOST /v1/webhook-subscriptions with the operations:read scope, an HTTPS callback URL, and a fresh 256-bit Idempotency-Key. The response includes the subscription ID and a { kid, secret } disclosure.
Store both values before accepting the response. If the response is lost, repeat the exact request with the same idempotency key. Perflo returns the same subscription, kid, and secret. Reusing that key for a different request returns an idempotency conflict. The binding is permanent.
This replay is the same logical disclosure, not a secret-recovery endpoint. Perflo stores only purpose- and context-bound Vault ciphertext and decrypts it only for an exact idempotency replay. GET /v1/webhook-subscriptions never returns secrets.
Perflo validates that every callback resolves only to public addresses. It repeats DNS and private-network validation for every delivery attempt and redirect. A subscription receives operation events created after its database-assigned creation boundary; it does not replay earlier events.
Verify a delivery
Read the request body as raw bytes before JSON parsing. Build the signed bytes in this exact order, with a line feed (\n) between fields:
Perflo-Webhook-Kid, encode the digest as unpadded base64url, and compare it with the value after v1= in Perflo-Webhook-Signature. Use a constant-time comparison.
This Node.js example verifies a body that your framework has preserved as a Buffer:
kid values, stale timestamps according to your replay policy, and invalid signatures before processing the JSON payload.
Process events idempotently
The body is canonical JSON with this shape:id as your idempotency key. Delivery is at least once, so a successful event can arrive more than once after a lost acknowledgement or worker restart.
Perflo preserves contiguous ordering for one subscription and operation. It can deliver events for unrelated operations in parallel, so it does not provide global ordering. Return a 2xx response only after durably recording the event.
Rotate a secret
CallPOST /v1/webhook-subscriptions/{id}/secret-rotations with a fresh 256-bit Idempotency-Key to create a new { kid, secret }. If the response is lost, repeat the exact request with the same key to recover the same logical disclosure. An equal replay never rotates again; a different request with that key conflicts permanently.
New events pin the active kid. An event already queued before rotation keeps its earlier kid and secret for every retry. Keep both secret versions available while either kid may appear. Perflo retains a retired encrypted secret for at least 24 hours and longer when a nonterminal delivery still references it.
Handle retries and dead letters
Perflo retries transient DNS, connection, TLS, timeout,408, 425, 429, and 5xx failures. The first retry waits 10 seconds. Later intervals increase exponentially and are capped at one hour. After eight failed attempts, Perflo dead-letters the delivery and releases the next event for that operation.
Retries preserve the body, event identity, version, and kid. The timestamp and signature change on each attempt. Return another 2xx response for a duplicate event after confirming that the original processing completed.
Delete a subscription with DELETE /v1/webhook-subscriptions/{id} when it should no longer receive events. Deletion stops new fanout and dead-letters its nonterminal deliveries.