Outcome
Your HTTPS handler verifies exact raw bytes before parsing, durably deduplicates events, acknowledges after persistence, and refetches authoritative state.Prerequisites
- A public HTTPS endpoint without redirects or private/reserved resolution.
- A Bearer key.
- Secure secret storage, a replay window, and a durable event-ID receipt table.
- One stable idempotency key per create, test, redelivery, or rotation command.
1. Create and capture the one-time secret
The destinationhttps://hooks.example.test/paywise is a placeholder like the
API key: replace it — here and in the runnable workflow below — with your own
publicly reachable HTTPS endpoint before running the sample. The API resolves
the hostname at creation time and rejects unreachable or private destinations
with 400 validation_error, so the placeholder itself never creates a
subscription.
CaseWebhookWithSecret.secret_key appears exactly once. Import it directly
from the restricted response into a secret manager and securely remove the
response; never print or log it. Ordinary read/update responses never expose
the secret.
2. Exhaust the secret-free subscription read
Representative response
PaginatedWebhookList response correctly contains no secret.
3. Request a test delivery
delivery_id and event_id. A fresh request ID is
not a delivery correlation key.
4. Read the stable delivery detail
5. Redeliver only after exact ownership validation
id and exact webhook equal the expected
values. The redelivery POST occurs only after that check.
Redeliver only a settled delivery — status success or failed. While a
delivery is pending, retrying, or delivering, the dispatcher still owns
it and the command is 409 delivery_in_progress; wait for it to settle and
read the outcome before deciding. The command takes no request body
(400 unexpected_body otherwise), and its expensive:webhook-delivery
budget unit is charged only when the 202 is returned.
6. Rotate and retain overlap secrets
secret_key once. Try the current secret first and retain the
prior secret for the documented 24-hour overlap. Every delivery for the
contract_version=v2 subscription used in this workflow, including retries
created before rotation, carries newest and previous signatures during that
window. Remove the prior secret after the overlap.
Exact raw-body verification and durable receipt
Readwebhook-id, webhook-timestamp, and webhook-signature before parsing.
The signed bytes are exactly
<event_id>.<unix_timestamp>.<exact raw JSON>. Split the space-separated
v1,<base64> signatures, strictly Base64-decode and validate every entry,
then accept any raw HMAC-SHA256 digest matching a held secret in constant
time. Enforce the replay window, then parse JSON and require payload id to
equal the header ID. Compatibility aliases are not additional signatures.
Persist the event ID, raw/minimized receipt, and one pending outbox job in the
same durable transaction before returning 2xx. A duplicate receives success
only after the prior receipt and job are confirmed. A separate recoverable
worker claims pending or interrupted jobs, performs an idempotent domain
effect, and marks the job complete. It refetches authoritative state by stable
identity; delivery retries are not a job queue.
For acceptance, reconcile the stable claim UUID you stored when the draft was
created. The submitted order ID is informational after review: paywise may
accept the claim under a different order without changing the claim UUID.
Match membership in claim_ids, then read the claim directly and adopt its
current order_id and mandate_id:
Acceptance reconciliation
merged_into for claim commands.
Every claim-specific read or command continues at its
top-level /v2/claims/{claim_id}/… URL.
Runnable Python workflow
Python workflow
WEBHOOK_PHASE=management. Deploy the HTTP receiver with
WEBHOOK_PHASE=handler and the recoverable job runner with
WEBHOOK_PHASE=worker, both pointing at the same durable database. Handler and
worker invocations make no paywise management API calls and need no API URL or
API token. Supply the captured values to the receiver as
WEBHOOK_CURRENT_SECRET and WEBHOOK_PREVIOUS_SECRET; the worker needs neither.
Each invocation is independent: a worker crash is recovered by a later fresh
worker execution, not an in-memory retry. There is deliberately no combined or
default phase; an omitted or unknown phase fails closed.
The subscription search uses a five-page example budget. Keep a finite
production budget plus cycle and exact HTTPS-origin validation before every
opaque next request so the Bearer token is never sent elsewhere.
Failure and recovery
- Invalid signature, mismatched ID, or stale timestamp: reject before domain parsing/processing and record only safe diagnostics.
- Duplicate ID: acknowledge only after confirming the durable prior receipt.
- Out-of-order event: persist, acknowledge, and refetch current state.
- Management
429: test and redelivery share a company bucket; wait at leastRetry-After, then retry the same command and idempotency key. 409 delivery_in_progress: the delivery is stillpending,retrying, ordelivering. Do not retry blindly; poll the delivery until it settles.409 webhook_limit_reached: the company already holds 20 endpoints (enabled or disabled). The cap is enforced atomically, so parallel creates cannot slip past it; delete an unused endpoint first.400 unexpected_bodyonrotate-secretorredeliver: send the command without a body.403 use_partner_webhooks: a Partner key withX-On-Behalf-Of-Companycannot create, change, or redeliver company endpoints; this answer comes before anyIf-Match(412) or query-parameter (400) check. Use the Partner-owned endpoints instead.- Management timeout,
500, or503: retry the same POST and key.
Verify
event_id equals the test command’s event_id to
report status success and a 2xx response_status_code, and confirm
your receiver stored exactly one receipt for that event UUID — a second
receipt means deduplication did not hold.
