Outcome contract | Verification | Retry handling
Audience |
Merchants, developers, and integration engineers who build or operate webhook consumers for Zahlen events. |
Version 1.0 | Source baseline: zahlen_deploy_0616A.tar.gz | June 2026 Commercial developer experience | Tenant-safe operations | Explainable retry intelligence
Learning objectives |
By the end of this chapter, you should be able to create and manage webhook subscriptions, interpret the outcome-delivery contract, verify deliveries using the active deployment policy, and implement retry-safe, duplicate-safe processing. |
Webhooks allow Zahlen to send event notifications to a merchant-controlled HTTPS endpoint. Instead of repeatedly polling for a change, the merchant registers a callback URL and one or more event types. When a subscribed event occurs, Zahlen can deliver a request to that callback according to the active deployment contract.
The confirmed merchant-facing subscription surface contains three operations:
Method | Path | Purpose |
POST | /v1/webhook-subscriptions | Create a tenant-scoped subscription. |
GET | /v1/webhook-subscriptions | List subscriptions visible to the authenticated merchant. |
DELETE | /v1/webhook-subscriptions/ {subscription_id} | Delete or deactivate a subscription. |
Contract boundary |
The uploaded schema confirms subscription management fields, but it does not define one universal delivery payload, signature algorithm, signing header, or retry timetable. Production clients must obtain the active webhook outcome contract and verification policy from their Zahlen deployment. |
Why webhooks matter
In the Zahlen commercial workflow, a decision is not the same as an observed result. Webhooks can notify downstream systems that durable outcome or operational evidence is available, reducing polling and helping merchants keep their own records synchronized.
Use webhooks for notifications, not as the only system of record.
Persist durable Zahlen identifiers such as decision ID, request ID, outcome ID, event ID, and subscription ID.
Design for delayed, duplicated, and out-of-order delivery.
Keep payment scheduling separate from HTTP delivery retries. Webhook retries must never create extra payment attempts outside Zahlen's fixed Day 1, Day 2, Day 6, and Day 16 schedule.
Subscription contract
All merchant-facing subscription calls use the X-API-Key header. Tenant and merchant ownership are derived from the authenticated key rather than trusted from the JSON body.
Create a subscription
curl -sS -X POST "$ZAHLEN_BASE_URL/v1/webhook-subscriptions" \
-H "Content-Type: application/json" \
-H "X-API-Key: $ZAHLEN_API_KEY" \
-d '{
"callback_url": "https://merchant.example.com/webhooks/zahlen", "events": ["REPLACE_WITH_ACTIVE_EVENT_TYPE"]
}'
Illustrative event value |
REPLACE_WITH_ACTIVE_EVENT_TYPE is intentionally not a real contract promise. Obtain the enabled event- type catalog from the deployed Zahlen outcome contract before creating a production subscription. |
Field | Type | Required | Constraints and meaning |
callback_url | string | Yes | Minimum length 8; maximum length 2,048. Use an HTTPS endpoint in production. |
events | array[string] | Yes | At least 1 and at most 20 event-type strings. |
Unknown top-level properties are rejected because the create model forbids extra fields. This protects integrations from silently sending misspelled or unsupported configuration.
Create response
{
"subscription_id": "whsub_01J...", "merchant_id": "merchant_example", "tenant_id": "tenant_example",
"callback_url": "https://merchant.example.com/webhooks/zahlen", "events": ["REPLACE_WITH_ACTIVE_EVENT_TYPE"],
"status": "ACTIVE",
"created_at": "2026-06-16T14:00:00Z", "updated_at": "2026-06-16T14:00:00Z",
"deleted_at": null
}
Example values |
Identifier formats and status values above are illustrative. Parse the documented fields, but do not hard-code invented prefixes or status catalogs unless your deployment contract defines them. |
Response fields and lifecycle
Field | Type | Required | Client use |
subscription_id | string | Yes | Stable identifier for later deletion, logs, support, and audit. |
merchant_id | string | Yes | Merchant context resolved by Zahlen. |
tenant_id | string | Yes | Tenant ownership resolved from authentication. |
callback_url | string | Yes | Registered destination. |
events | array[string] | Yes | Subscribed event types. |
status | string | Yes | Current subscription state. |
created_at | string | Yes | Creation timestamp. |
updated_at | string | Yes | Most recent update timestamp. |
deleted_at | string or null | No | Deletion/deactivation timestamp when applicable. |
List subscriptions
curl -sS "$ZAHLEN_BASE_URL/v1/webhook-subscriptions" \
-H "X-API-Key: $ZAHLEN_API_KEY"
The list response contains merchant_id, tenant_id, count, and subscriptions. Treat an empty list as a valid tenant-scoped result; do not bypass tenant filters to find subscriptions belonging to another account.
Delete or deactivate a subscription
curl -sS -X DELETE \
"$ZAHLEN_BASE_URL/v1/webhook-subscriptions/whsub_01J..." \
-H "X-API-Key: $ZAHLEN_API_KEY"
Delete response field | Type | Meaning |
subscription_id | string | Subscription acted upon. |
merchant_id | string | Authenticated merchant context. |
tenant_id | string | Authenticated tenant context. |
deleted | boolean | Whether deletion/deactivation occurred. |
status | string | Resulting subscription status. |
deleted_at | string or null | Deletion time when supplied. |
Operational rule |
Store subscription_id locally. Do not discover it by matching callback URLs during an outage or deployment change. |
Outcome contract
The outcome contract describes what Zahlen sends, when it sends it, how the receiver identifies the event, and how authenticity is verified. It is distinct from the subscription-management schema.
Contract area | Questions the production contract must answer |
Event catalog | Which event-type strings may be subscribed to? Which versions are active? |
HTTP request | Which method, content type, timeout, and headers are used? |
Envelope | Where are event ID, delivery ID, event type, version, and creation time located? |
Payload | Which outcome, decision, payment-event, and merchant correlation fields are included? |
Verification | Which signature algorithm, secret, header names, timestamp rules, and canonical byte sequence apply? |
Acknowledgment | Which HTTP response codes count as successful delivery? |
Retry policy | Which failures are retried, how often, and for how long? |
Replay | How can an authorized operator replay a failed delivery? |
Recommended consumer envelope
A robust client should be able to process an envelope with stable delivery metadata and a versioned payload. The following structure is conceptual only and must not replace the active deployment contract:
{
"event_id": "provider-defined stable event identifier", "delivery_id": "provider-defined delivery attempt identifier", "event_type": "contract-defined event type", "schema_version": "contract-defined version",
"created_at": "ISO-8601 timestamp", "data": { "contract-defined payload": true }
}
Do not guess |
Do not infer field names, event names, or signature headers from this conceptual envelope. Generate production parsing and verification from the actual Zahlen outcome contract. |
Correlation with retry outcomes
Where the active event carries retry-outcome evidence, correlate it using durable identifiers instead of customer-readable labels. The retry outcome API can expose outcome_id, request_id, decision_id, token, attempt_number, outcome, processor evidence, timestamp, and matched_by. A webhook consumer should store whichever of these fields are included by the active contract and avoid declaring recovery based only on a scheduled attempt.
Verification
Verification proves that an inbound request was created by the expected Zahlen deployment and was not modified in transit. Because the 0616A subscription schema does not define a universal signing mechanism, the steps below describe the required security pattern without inventing algorithm-specific details.
Read the exact raw request body before JSON parsing or reformatting.
Read the contract-defined signature, timestamp, key identifier, and version headers.
Reject missing or unsupported verification metadata.
Check that the delivery timestamp falls within the allowed replay window.
Compute the expected signature using the contract-defined algorithm and canonical input.
Compare signatures using a constant-time comparison function.
Deduplicate the stable event or delivery identifier before applying business effects.
Only then parse and process the payload.
Raw bytes matter |
Many signing schemes authenticate the exact HTTP body bytes. Parsing JSON and serializing it again can change whitespace or property ordering and cause valid signatures to fail. |
Illustrative verification pseudocode
raw_body = request.read_raw_bytes()
metadata = read_contract_headers(request.headers)
if metadata.missing_or_unsupported():
return HTTP_401
if timestamp_outside_allowed_window(metadata.timestamp): return HTTP_401
expected = contract_sign(secret, metadata, raw_body)
if not constant_time_equal(expected, metadata.signature): return HTTP_401
envelope = parse_json(raw_body)
if already_processed(envelope.stable_event_id): return HTTP_200
enqueue_for_processing(envelope) return HTTP_200
Secret management
Store verification secrets in a secret manager or protected environment variable.
Do not log signature secrets, API keys, or complete authorization headers.
Support secret rotation with an overlap window or key identifier when the contract provides one.
Use separate verification material for development, staging, and production.
Restrict callback endpoints to HTTPS and maintain valid TLS configuration.
Reliable consumer architecture
The callback handler should do as little synchronous work as possible. Long-running business logic increases timeout risk and can cause Zahlen to retry a delivery that actually reached your system.
Stage | Responsibility | Failure behavior |
1. Receive | Accept HTTPS request and retain raw bytes. | Reject malformed transport safely. |
2. Verify | Authenticate signature and replay timestamp. | Return the contract-defined authentication failure. |
3. Deduplicate | Reserve stable event ID in durable storage. | Previously completed event returns success without reapplying effects. |
4. Persist | Store envelope, headers needed for audit, and receive time. | Do not acknowledge if durable acceptance failed. |
5. Enqueue | Place work on an internal queue. | Use transactional outbox/inbox patterns where practical. |
6. Acknowledge | Return success quickly. | Use only success codes recognized by the active contract. |
7. Process | Apply idempotent business logic asynchronously. | Retry internally without requiring another external delivery. |
Separate receipt from business completion |
A successful webhook response should ordinarily mean the event was verified and durably accepted, not that every downstream workflow has finished. |
Deduplication record
Stored value | Purpose |
stable_event_id | Prevents duplicate business effects across repeated deliveries. |
delivery_id | Supports per-attempt diagnostics when the contract supplies it. |
event_type and schema_version | Selects the correct parser and handler. |
received_at and processed_at | Measures delivery and processing lag. |
payload hash | Supports integrity and duplicate diagnostics without storing excess sensitive data. |
processing status and error | Supports internal retry and operations. |
Retry handling
Webhook retry handling has two sides: Zahlen may retry delivery when the callback is unavailable, and the merchant may retry internal processing after the callback has been durably accepted. Both sides must be duplicate-safe.
Provider delivery retries
Expect the same logical event more than once.
Do not use arrival count as a business quantity.
Return the contract-defined success response for an already-processed event.
Do not deliberately fail duplicates to force another delivery.
Use administrative operations for authorized replay rather than editing delivery records directly.
Merchant internal retries
Retry downstream database, queue, notification, or reporting work using the stored event ID.
Make every side effect idempotent: use unique keys, compare-and-set transitions, or transactional records.
Apply bounded exponential backoff with jitter to transient internal failures.
Move repeatedly failing work to a dead-letter or manual-review queue without losing the original envelope.
delay = min(max_delay, base_delay * (2 ** retry_number))
delay = delay * random.uniform(0.75, 1.25)
HTTP response guidance
Condition | Typical receiver behavior | Why |
Valid and durably accepted | Return an allowed success code quickly. | Prevents unnecessary provider retries. |
Valid duplicate already processed | Return an allowed success code. | Duplicate delivery is normal network behavior. |
Invalid signature or stale replay | Return the contract-defined authentication failure. | Do not process unauthenticated content. |
Malformed payload | Return the contract-defined client failure and quarantine evidence. | Blind retries usually cannot repair invalid content. |
Temporary inability to persist | Return a retryable failure only if the contract defines it. | Provider retry may recover a transient outage. |
Downstream business system unavailable after durable acceptance | Acknowledge and retry internally. | Avoid coupling callback latency to downstream health. |
Payment retry boundary |
A duplicate or delayed webhook must not trigger an additional card authorization. Payment attempts remain governed by the fixed Day 1, Day 2, Day 6, and Day 16 schedule and the explicit Zahlen decision/outcome workflow. |
Ordering, versions, and schema evolution
Network delivery does not guarantee that related events arrive in the order they were created. Consumers should use timestamps, versions, and durable state transitions instead of assuming arrival order is business order.
Ignore or quarantine unsupported event types rather than treating them as a known event.
Route each schema_version to a compatible parser when version metadata exists.
Allow additive optional fields without breaking parsing, while still validating required fields.
Do not overwrite newer local state with an older event solely because it arrived later.
Keep contract tests for current and prior supported payload versions.
Test plan
Test | Expected result |
Valid signed delivery | Verified, stored, queued, and acknowledged once. |
Duplicate valid delivery | No duplicate business effect; successful acknowledgment. |
Invalid signature | Rejected before payload processing. |
Missing verification metadata | Rejected according to the active contract. |
Old replay timestamp | Rejected outside allowed replay window. |
Unknown event type | Safely ignored or quarantined with an operational signal. |
Out-of-order related events | State remains correct and does not regress. |
Temporary database outage before persistence | Retryable response according to contract. |
Downstream outage after persistence | Callback succeeds; internal work retries. |
Consumer timeout | No partial untracked business effect. |
Secret rotation overlap | Deliveries signed with permitted current/previous keys verify correctly. |
Deleted subscription | No new deliveries expected after contract-defined deactivation behavior. |
Implementation examples

from flask import Flask, request, jsonify app = Flask( name )
@app.post("/webhooks/zahlen") def zahlen_webhook():
raw_body = request.get_data(cache=True)
metadata = read_contract_headers(request.headers)
if not verify_with_active_contract(raw_body, metadata): return jsonify({"accepted": False}), 401
envelope = parse_and_validate_contract_payload(raw_body) event_id = stable_event_id(envelope)
if inbox_already_completed(event_id):
return jsonify({"accepted": True, "duplicate": True}), 200
persist_and_enqueue_atomically(event_id, envelope, metadata) return jsonify({"accepted": True}), 200
Python callback skeleton
JavaScript callback skeleton
app.post("/webhooks/zahlen", rawBodyMiddleware, async (req, res) => { const rawBody = req.body;
const metadata = readContractHeaders(req.headers);
if (!verifyWithActiveContract(rawBody, metadata)) { return res.status(401).json({ accepted: false });
}
const envelope = parseAndValidateContractPayload(rawBody); const eventId = stableEventId(envelope);
if (await inboxAlreadyCompleted(eventId)) {
return res.status(200).json({ accepted: true, duplicate: true });
}
await persistAndEnqueueAtomically(eventId, envelope, metadata); return res.status(200).json({ accepted: true });
});
Framework warning |
Some web frameworks parse JSON before your handler runs. Configure raw-body capture for the webhook route if the active verification algorithm signs the exact request bytes. |
Operations and troubleshooting
Symptom | Likely checks |
No deliveries | Subscription status, enabled event types, callback URL, tenant/key context, and whether the triggering event occurred. |
Repeated deliveries | Receiver timeout, non-success response, persistence failure, or expected at-least-once behavior. |
Every signature fails | Wrong environment secret, body mutation, incorrect canonical input, clock skew, or unsupported contract version. |
Some signatures fail | Secret rotation, multiple key IDs, proxy body transformation, or timestamp parsing. |
High processing lag | Synchronous callback work, queue backlog, downstream dependency failure, or insufficient workers. |
Duplicate business actions | Missing durable inbox key or non-idempotent downstream operation. |
Events appear out of order | Normal network behavior; use event time/version and monotonic state transitions. |
Deleted endpoint still receives requests | In-flight delivery, deactivation semantics, cache/propagation delay, or another active subscription. |
Production readiness checklist
Callback URL uses HTTPS and has valid TLS.
Production event types come from the active Zahlen outcome contract.
Raw request body is retained long enough for verification.
Verification rejects missing, invalid, and stale authentication metadata.
Secrets are stored securely and rotation is tested.
Durable deduplication is performed before business effects.
Callback persistence and acknowledgment complete quickly.
Downstream work is asynchronous and independently retryable.
Unknown event types and schema versions are quarantined safely.
Duplicate, delayed, out-of-order, and replayed deliveries are tested.
Alerts cover signature failures, delivery failure rate, processing lag, and dead-letter growth.
Webhook handling cannot create payment attempts outside Day 1, Day 2, Day 6, and Day 16.
Chapter summary |
Create, list, and delete subscriptions with X-API-Key. Treat the deployed outcome contract as authoritative for event names, payloads, verification, acknowledgments, and retry policy. Verify raw deliveries, persist before acknowledging, deduplicate with durable identifiers, process asynchronously, and keep webhook retries completely separate from payment retries. |