Skip to main content
Webhooks send signed HTTP notifications when something changes in paywise. This page covers the behavior shared by all webhook subscriptions: receiving an event, verifying it, processing it reliably, and recovering from failures. If a delivery is missing or failing, start with Troubleshooting. Choose event types and subscription settings in the docs for your API:

Test a sandbox delivery

This example uses a company subscription and order.accepted. For a Partner subscription, follow Consume Partner webhooks; the receiver and verification steps are the same.
  1. Open webhook.site and copy your unique HTTPS URL, or deploy your own public HTTPS test receiver. Use synthetic sandbox data with a disposable third-party receiver.
  2. In the sandbox portal, open Für Entwickler → Webhooks → Neuen Endpoint hinzufügen, enter that URL, and select order.accepted. You can also create it with POST /v2/webhooks/ using your sandbox key; save the one-time secret_key from the response for verification.
  3. Send a test delivery with POST /v2/webhooks/{id}/test/ and JSON body {"event": "order.accepted"}, or finalize an order and simulate acceptance to exercise a real lifecycle event.
  4. Inspect the incoming body and signature headers in your receiver and the delivery result in the portal. A synthetic test confirms transport; the simulated acceptance also lets your handler fetch the real order and mandate.
The sandbox has no built-in webhook-bin receiver. A localhost or private-IP URL cannot receive deliveries directly. To exercise your local handler, expose it through a public HTTPS tunnel whose URL answers without redirects, then register that URL. Use the event feed to reconcile missed events; it does not verify the HTTP push path or your signature handling. Synthetic test deliveries use the selected event’s payload shape with generated resource identifiers. They contain no test marker. Correlate a test with the event_id and delivery_id returned by the command; a test does not switch the subscription’s environment.

Receive an event

Deliveries contain a stable event UUID and a thin reference to the affected resource. This example body comes from the current Case contract:
Every event has four top-level fields:

Verify signatures

The canonical verification headers are webhook-id, webhook-timestamp, and webhook-signature. X-Paywise-Signature and X-Paywise-Timestamp carry the same signature and timestamp as compatibility aliases. To verify a delivery:
  1. Preserve the exact request-body bytes and read the three canonical headers.
  2. Build the UTF-8 message <webhook-id>.<webhook-timestamp>.<raw-body>. Do not reserialize the JSON.
  3. Compute HMAC-SHA256 with the endpoint’s signing secret encoded as UTF-8, Base64-encode the raw 32-byte digest, and prefix it with v1,.
  4. Split webhook-signature on spaces and strictly decode and validate every v1,<base64> entry. Accept any digest matching a held secret using constant-time comparison before parsing the body. Confirm that webhook-id equals the payload id, and reject a timestamp outside your chosen replay window.

Verify with a known answer

Check your implementation against this vector before pointing it at real deliveries. Every input is shown verbatim; the body is exactly 49 bytes with no trailing newline.
If your result differs, the usual causes are a reserialized body (added whitespace or reordered keys), a missing dot between the three parts, a hex-encoded instead of Base64-encoded digest, or a secret that was itself Base64-decoded before use. During a rotation overlap the same delivery carries v1,cePL6RRZ5U5l9c1ik1KH+BCyH0Zd+CO0Rj2dZej25No= v1,<signature with the previous secret>; this known-answer vector matches the first entry.

Signing secrets

For a contract_version=v2 subscription, every delivery during the 24-hour rotation overlap carries the newest and previous signatures in one space-separated header: v1,<new-signature> v1,<previous-signature>. This includes retries of deliveries created before rotation. Strictly Base64-decode and validate every entry, then accept any signature matching a secret you hold using constant-time comparison. Keep both secrets for the overlap and try the current secret first; afterward, deliveries carry only the newest signature. Never log a secret or a complete customer payload. A legacy contract_version=v1 subscription carries one signature made with its current secret rather than the v2 overlap pair.
POST /v2/webhooks/ and POST /partner/v2/webhooks/ return a documented one-time-secret response containing secret_key. The corresponding bodyless POST .../{id}/rotate-secret/ operations return the same one-time-secret shape. Capture and store that field securely. List, retrieve, and update responses use the secret-free webhook shape and never reveal it. Neither does an idempotent replay: repeating the create or rotate command with the same Idempotency-Key answers Idempotency-Replayed: true without secret_key. If the original response was lost, rotate the secret.

Process delivery safely

Delivery is at least once and unordered:
  1. Deduplicate by event UUID and durably store each verified event before returning 2xx. Acknowledge duplicates without applying them again.
  2. Process stored events asynchronously and record processing separately from HTTP receipt. Refetch the authoritative resource named by the thin payload; do not infer its current state from event arrival order.
  3. Handle missing or deleted resources. If processing fails, retry it independently; after durable receipt, do not return a failing HTTP status to use paywise delivery retries as a job queue.
  4. Tolerate growth. Store and acknowledge an event whose type you do not recognise, then skip it; never fail the delivery. Ignore data members you do not know.
The HTTP transport has 5 seconds to connect and 10 seconds in total. Synchronous DNS resolution and, for Partner webhooks, Partner authorization happen before and outside that transport budget. Timeouts, connection errors, transient TLS errors, DNS resolution failures, 429, and 5xx are retried for no more than 48 hours. After a transient failure, that endpoint’s other due retries wait for the next retry run, at most about 60 seconds later; the delivery’s exponential backoff still applies. Redirects, 4xx responses other than 429, a TLS certificate or hostname mismatch, and a destination that resolves to a non-public address are terminal. The delivery’s error_message names the reason, for example Request timed out after 10 seconds, Connection error: <type>, TLS error: <type>, TLS certificate error: <type>, blocked_destination:unresolvable (retried), or blocked_destination:internal_address (terminal). Manual redelivery reuses the original UUID and payload. It does not create a new domain event.

Event feed

Use the ordered Case feed or Partner feed to catch up after missed webhooks. Events remain available for 30 days and appear about 60 seconds after paywise records them. Each page returns cursor, the position after its last event: store it and pass it as since on your next poll. On an empty page cursor echoes the since you sent; it is null only for an empty page requested without since. next is set only while more events are waiting — follow it to drain a backlog. The feed is ordered by the time paywise recorded each event, so an event’s created_at can be earlier than that of the event before it. limit accepts ASCII digits from 1 to 100. Deduplicate by the same event id used in webhooks.

Recover missed events

The event feed is the recovery path; manual redelivery is for a single event. paywise has no bulk redelivery. The feed keeps events for 30 days, so store your cursor durably and poll it at least that often. In the paywise portal, Erneut senden on the delivery log re-sends failed deliveries up to 7 days old. Older deliveries show a disabled button. Use the API’s redeliver, which has no age limit, or the feed.

Subscription and delivery recovery

If events is omitted at creation, paywise stores the complete catalog that exists then. Literal "*" also includes future events available to that subscription. See your API’s subscription details for ownership and filtering. There is no generic catch-all event. Deleting a subscription hard-deletes its delivery rows. Each company (Case Management API) or Partner (Partner API) may register at most 20 endpoints, enabled or disabled; creating a 21st returns 409 with code webhook_limit_reached — delete an endpoint you no longer need first. The cap is enforced atomically, so concurrent creates never exceed it. Two of your endpoints may not share a destination: scheme and host compare case-insensitively and the default port is ignored, while path and query must match exactly. A create or PATCH that would collide returns 400 with duplicate_url on the url field. Use distinct paths or query strings when you deliberately need several subscriptions on one host. Destinations must use HTTPS, resolve only to public unicast IPs (multicast, loopback, private, link-local, and cloud-metadata addresses are refused), and must not redirect. DNS and address safety are checked again for every delivery attempt: paywise resolves the host anew and connects only to an address from that resolution, so a destination behind a load balancer or DNS pool may change addresses between attempts. The stored url is the canonical form of what you sent — lower-case scheme and host, IDNA-encoded host, trailing FQDN dot and default port removed, path and query byte-exact — so read it back from the response instead of assuming your input. A fragment is 400 fragment_not_allowed, a port outside 1–65535 is 400 invalid_port, and a host that cannot be IDNA-encoded is 400 invalid_host. A public IPv4 literal such as https://93.184.216.34/hook is accepted. A missing, dotless, or localhost host, an IPv6 literal, and a host or IPv4 literal that resolves to a non-public address are 400 on url with code invalid (The webhook URL must be a publicly reachable HTTPS URL.). enabled accepts only a JSON boolean and max_consecutive_failures only a JSON integer from 1 to 1000 (default 50, on Case and Partner endpoints alike); description is a single line. When an endpoint reaches max_consecutive_failures, paywise switches it off and the read shape reports auto_disabled: true next to enabled: false, consecutive_failures, and last_failure_at. Fix the destination, then PATCH {"enabled": true}: the re-enable resets the failure counter, so the next terminal failure does not immediately disable it again. A PATCH that changes nothing leaves updated_at and the ETag untouched. The list filter ?enabled= accepts only true, false, 1, or 0. rotate-secret and redeliver take no body; sending one is 400 unexpected_body. Sandbox and production use the same destination and signature rules. Test delivery and manual redelivery share a limit of 20 commands per hour for each company or Partner, charged only when the command is accepted (202); see Public limits. Both commands return 202 Accepted with detail, delivery_id, and event_id. Manual redelivery creates a new delivery attempt (delivery_id) while keeping the original domain event UUID (event_id) and payload. It is allowed only on a settled delivery (success or failed, including one whose 48-hour retry window has expired); while a delivery is still pending, retrying, or delivering the command is 409 delivery_in_progress — wait for it to settle. A disabled endpoint answers 409 conflict. Partner-owned subscriptions are re-checked at delivery time, not only when the event is fanned out: a queued case event for a company whose Partner access has ended is closed as failed with error_message partner_access_unavailable, without contacting your endpoint and without counting toward consecutive_failures. This covers queued lifecycle company.* events as well; only your own company.access.revoked is always delivered. See Partner webhook ownership. The access check runs right before each send and does not hold up a disconnect: an attempt that has already passed it can still reach your endpoint just after the disconnect commits. Every later attempt, retry, and manual redelivery is suppressed. Mahnservice events are durable. Most invoice.* and dunning.* events are recorded together with the invoice change that raises them. A real debt-collection handover instead commits a durable handover record, and a relay records dunning.handed_to_collection under its stable event ID shortly afterward. Delivery itself stays asynchronous. See the Mahnservice event details.

Troubleshooting

Start with the delivery log in the developer portal (Für Entwickler → your webhook). It shows each attempt, its response status, and up to 4 KiB of your response body for 30 days. Replay resends the original event UUID and payload. For Partner-owned subscriptions, use the Partner delivery history.

Contact support

If these checks do not resolve the problem, contact [email protected] with the subscription ID, the event UUID if available, and the X-Paywise-Request-Id of the API call that should have produced the event. Do not include signing secrets or complete customer payloads.