Skip to main content

Outcome

Your HTTPS handler verifies Partner lifecycle signatures before parsing, durably deduplicates at-least-once events, temporarily gates delegated writes for access events, and refetches authoritative company state independently of delivery order.

Prerequisites

  • A public HTTPS endpoint that neither redirects nor resolves to a private or reserved address.
  • A Partner key.
  • Secure secret storage, constant-time HMAC comparison, a replay window, and a durable table keyed by event UUID.
  • Stable idempotency keys for create, test, rotate, or manual-redelivery commands.

Lifecycle context

Partner subscriptions receive company lifecycle events and, with a companies selector, Case events for connected companies. This example uses companies: "*" to include current and future connections. Readiness emits only when its boolean changes. Confirmation emits once per authorization version. Revocation is the final event whose creation and signature are authorized by the previous state, not necessarily the last delivery chronologically. Restoration resumes future events without replay. Delivery is at least once and unordered.

1. Install the sandbox guard

The function makes an authenticated safe read and fails closed if the exact case-insensitive environment header is absent or not sandbox.

2. Create a Partner-owned subscription

The create response uses the documented PartnerWebhookWithSecret shape and contains secret_key exactly once. Import .secret_key directly from the restricted response file into a secrets manager, then securely remove the file. List, retrieve, and update responses never reveal it. See the shared one-time-secret rule. Never print or log the secret. Omitting events subscribes to the Partner lifecycle catalog that exists at creation. The machine contract supports "events":["*"], with the wildcard as the only element. With companies: "*", it includes all supported company and Case events, including future event types. Replace it with explicit event names when you need only selected updates; never combine the wildcard with names. The test command’s event field remains limited to one concrete event from the Partner catalog.

3. Confirm the secret-free read model

Representative response

This complete response matches PaginatedPartnerWebhookList and correctly contains no secret.

4. Verify exact delivery bytes

Read webhook-id, webhook-timestamp, and webhook-signature before parsing. Preserve the exact body bytes and build the UTF-8 message:
Compute HMAC-SHA256 using the endpoint secret as UTF-8, Base64-encode the raw 32-byte digest and compare in constant time against the header’s space-separated v1,<base64> entries. Strictly decode and validate every entry before accepting any signature matching a held secret. Confirm the header ID equals payload id and reject timestamps outside your replay window. X-Paywise-Signature and X-Paywise-Timestamp are compatibility aliases, not additional signatures. In one durable receipt transaction, insert the event UUID and processing record. Return 2xx after durable acceptance, including for an already stored duplicate, then refetch data.company_url asynchronously. Never use webhook retry delivery as the application’s job queue.

5. Prove the sandbox before requesting a test delivery

6. Test with stable runtime identifiers

Capture delivery_id and event_id from this response. Read that exact delivery directly; never choose a list position.
The test command returns 202 Accepted with required detail, stable delivery_id, and event_id fields. The shared idempotency executor replays that body for an exact retry while it is retained, so reuse the same key and body after a timeout. The response’s fresh X-Paywise-Request-Id is not a delivery or execution identifier. Retrieve the exact runtime delivery_id; do not choose a delivery list’s first row. Multiple subscriptions may legitimately create several delivery records for the same event UUID. The authenticated Partner-scoped detail endpoint is the ownership boundary: require its returned id and event_id to match the two identifiers captured from the test command. A delivery detail does not carry a subscription identifier.

7. Prove the sandbox before manual redelivery

8. Redeliver an exact retained delivery

Use a source delivery UUID captured from a command response or durable delivery record, never a list position. Runtime creates a new delivery UUID while preserving the original event UUID and payload:
The representative command response is:
This documented response matches PartnerWebhookCommandResult. Correlate retries with its stable new delivery_id and preserved event_id, and do not substitute the fresh response request ID. An exact retry uses the same idempotency key and body.

9. Prove the sandbox before rotating the secret

10. Rotate with a 24-hour overlap

Rotate is a bodyless command. Capture its documented one-time-secret response:
Store the new secret_key and retain the prior secret for the 24-hour overlap. Every delivery for the v2 subscription in that window, including retries created before rotation, carries newest and previous signatures. Try the current secret first. A second rotation during the overlap returns 409; do not discard the prior secret early.

Complete runnable Python workflow

Run management commands only in the management phase. The HTTP receiver and worker share the same durable database, but neither needs Partner API credentials or makes management requests.
Python workflow
The management phase follows at most five pages and validates the exact HTTPS origin, credentials, and cycle budget before every opaque next request. Create and rotation secrets go only to mode-0600 unpredictable handoff files and never to output. Deploy handler and worker as independent processes against the same durable database; delivery retries are not the worker queue.

Failure and recovery

  • Invalid signature, mismatched ID, or stale timestamp: reject before parsing or processing and record only safe diagnostics.
  • Duplicate event: acknowledge after confirming prior durable receipt; apply the business effect once, but still reconcile current state.
  • Any access event: durably accept and deduplicate, temporarily gate delegated writes, and fetch the exact current Company. Apply unavailable cleanup only when current case_access is unavailable; preserve or restore traffic when it is available, even if a delayed revocation triggered reconciliation.
  • Out-of-order event: never roll access or readiness backward from arrival order, and never use authorization_version as an ordering clock.
  • Handler network failure, 429, or 5xx: paywise retries for at most 48 hours. Redirects and other 4xx responses are terminal.
  • Management timeout or transient 5xx: make at most one replay with the same POST body and idempotency key. For 429, accept only an integer Retry-After from 1 through 60 seconds, wait that exact interval, and make the same single bounded replay; fail closed on a missing, malformed, or excessive value.
  • Missed restoration: restoration does not replay missed events or deleted drafts. Run periodic current-state reconciliation so a stale local gate cannot strand future traffic.
  • Manual redelivery: reuse only an exact retained delivery UUID. Duplicate destinations and redeliveries make business-field or first-row selection unsafe. Redeliver settled deliveries only: 409 delivery_in_progress means the delivery is still pending, retrying, or delivering — poll it until it settles. 404 on a delivery you retained means the company’s Partner access has ended since; the delivery is no longer yours to replay. rotate-secret and redeliver take no request body (400 unexpected_body).
  • Delivery failed with error_message: partner_access_unavailable: the company’s Partner access ended between queueing and delivery. Nothing was sent and the endpoint’s health is unaffected; reconcile the company with Handle access changes.
  • Missing invoice.* or dunning.* rows in GET /partner/v2/events/: verify access to the company and contact paywise support if expected events are unavailable.

Verify

Send a test event, retrieve its exact runtime delivery ID, and confirm your receipt table stores one row for the event UUID after an exact retry. Manually redeliver that exact delivery and confirm a new delivery ID carries the same event ID. During a rotation drill, accept deliveries with both allowed secrets, then remove the prior secret after 24 hours. Exercise all access-event types and confirm each one gates writes until current Company state is fetched.