Make your receiver safe when notifications are delayed, repeated or delivered out of order.
What you will learn
- Reconstruct the exact signature input.
- Acknowledge durable receipt and deduplicate business actions.
- Interpret delivery states, automatic attempts and dashboard Retry.
Prerequisites
- An active notification endpoint.
- Its signing secret in your backend's secure configuration.
- A durable database or queue under your control.
Request headers and raw body
The sender makes an HTTPS POST to the configured final URL.
| Header | Value | Receiver action |
|---|---|---|
| Content-Type | application/json | Read raw bytes before JSON middleware |
| User-Agent | LMP-Webhooks/1.0 | Diagnostic identifier, not authentication |
| X-LMP-Event-ID | evt_ followed by an opaque identifier | Match against authenticated body.id |
| X-LMP-Event | Event name | Match against authenticated body.event |
| X-LMP-Signature | t=decimal_unix_seconds,v1=lowercase_hex_hmac | Verify exact bytes with the endpoint secret |
The LMP names above are compatibility identifiers. Keep them unchanged even though the product is named KDPika Licensing.
The signed message is the ASCII decimal timestamp, one literal period byte, and the entire unmodified raw request body. The key is the plaintext endpoint signing secret; the algorithm is HMAC-SHA256 and the digest is lowercase hexadecimal with 64 characters. This is separate from the Ed25519 signature in a license response. Parsing and re-encoding JSON, trimming whitespace, changing Unicode escaping or sorting keys before verification invalidates the signature.
The body contains the common event envelope. The delivery timestamp is generated afresh for each attempt. The event ID, event.created_at and stored payload remain stable on retry. The current endpoint URL and current secret are used for the next attempt.
Receiver timestamp policy
The sender does not enforce your receiver's freshness policy. The verification examples require the signature timestamp to be within 300 seconds of the receiver clock in either direction. They impose a 1 MiB body limit. Both numbers are example receiver policy, not promises about a sender inbound limit or a new licensing API rate limit.
Synchronize the receiver clock. Check the signature timestamp, not event.created_at: a valid queued event can be older than 300 seconds when its next attempt is signed. A recent timestamp does not stop replay inside the acceptance window. Persist the authenticated event ID as well.
Durable acknowledgement and duplicate handling
- Read a bounded raw body.
- Verify the signature, freshness, expected environment and matching event headers.
- Look up your own resource/order mapping; do not trust a resource ID as authorization by itself.
- Atomically insert a unique authenticated event ID and a durable work item in your own database.
- Return a 2xx response after commit.
- Process the work item using a separately deduplicated business-operation key.
Expected result: a duplicate already accepted event returns a harmless 2xx without repeating a business effect. If durable storage fails, return a non-2xx response so delivery can retry. If an existing authenticated ID arrives with different body bytes, quarantine it for investigation rather than applying a second action.
Do not return 200 before persistence, and do not wait for a slow email provider or store API call before acknowledgement. A lost 2xx response can produce a retry even when your first handler succeeded. Exactly-once processing is not provided by HTTP or by the licensing API. In particular, create replay has concurrency and expiration limitations; renew is additive and not idempotent. Your inbox dedupe is necessary but does not make an uncertain downstream call safe to repeat.
Automatic attempts
| Failed attempt | Delay before the next eligible attempt |
|---|---|
| 1 | 60 seconds |
| 2 | 300 seconds |
| 3 | 1800 seconds |
| 4 | 7200 seconds |
| 5 | 43200 seconds |
| 6 | None; automatic attempts stop |
These delays are measured after failure handling. The maintenance scheduler processes due deliveries, so wall-clock delivery can be later than the minimum delay. At most six network attempts are made for one delivery row. A disabled endpoint or a row locked by another worker is skipped without consuming an attempt.
Success requires an HTTP status from 200 through 299 and no transport error. A 204 is sufficient; there is no required success-response body. Redirects, client errors, rate limits, server errors and transport failures are unsuccessful. Retry-After from a receiver does not replace the sender's fixed retry schedule.
Timeouts and network behavior
| Control | Actual behavior |
|---|---|
| Connection timeout | 5 seconds |
| Total request timeout | 12 seconds |
| Redirect following | Disabled |
| TLS certificate and hostname | Verified |
| Target address | Public DNS/IP validation with the validated address pinned for that attempt |
| Response body retained | At most 1000 bytes |
| Each response header value retained | At most 500 characters |
| Last error retained | At most 500 characters |
Return small generic receiver responses. Do not include a secret, complete license key or customer detail in an error response: an excerpt can be retained on the sender. A connection reset after your commit is still an uncertain acknowledgement and can cause a duplicate delivery.
Ordering and identifiers
There is no global ordering or exactly-once guarantee. A failed earlier event can arrive after a newer event succeeds. One business publication can use the same event ID for multiple subscribed endpoints; each has a separate delivery row. A repeated business operation can create a new event ID, so event-ID deduplication alone is not order/invoice deduplication.
For a current license view, fetch get license on your authenticated backend instead of blindly replacing it with an old event payload. For a side effect such as sending one purchase email, key the durable operation to your order/line/license mapping and the intended effect. Never let a delayed license.restored notification reverse a later revocation.
Delivery history and failure states
Open Webhooks and read Delivery history. The page shows the latest 250 delivery rows, not an append-only row for each historical attempt. Each row displays creation time, event name/ID, endpoint host, most recent HTTP status and duration, cumulative attempts, status, last error and next attempt time. Absence of an old row does not prove delivery never happened.
| Stored status | Meaning |
|---|---|
| PENDING | Queued, no completed network attempt yet |
| FAILED with a next attempt and count below 6 | A failed attempt eligible for a later retry |
| FAILED at count 6 | Attempt budget exhausted; no automatic recovery |
| DELIVERED | A 2xx response without transport error was received |
Delivered rows are eligible for maintenance deletion after 30 days. Failed and pending rows do not have that delivered-row cleanup condition. Retain only the minimal necessary receiver evidence under your own policy; dashboard history is not your billing ledger or backup.
Manual Retry is not unrestricted replay
- Fix your receiver problem.
- Check that the endpoint is Active.
- Find the failed row and its current attempt count.
- If the count is below six, select Retry and inspect the new result.
Expected result: an eligible row becomes due immediately and attempts once with its original event ID/body and a new signature timestamp. The attempt count is not reset. A Delivered row is not resent.
You can replay a captured synthetic request locally after generating a fresh test signature. That verifies your own handler only; it is not a platform delivery replay or a reason to resend a real payment event manually.
Expected result
The receiver safely acknowledges durable work, ignores identical duplicates, detects conflicting IDs and handles stale notifications without granting incorrect access.
Common mistakes
- Using the complete signature as the dedupe key; it changes on retry.
- Rejecting a delayed event because created_at is old even though its attempt signature is fresh.
- Assuming a 302 redirect or Retry-After controls delivery.
- Treating a successful HTTP response as proof that a customer can use a license.
Security notes
Pin organization context through endpoint configuration and environment through authenticated body checks. Protect inbox data from public downloads. Signature verification authenticates a notification, not a browser caller, a payment receipt or an application entitlement.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Attempt count does not increase | Disabled, already delivered, exhausted, locked or not yet due | Inspect status, count and next attempt before retrying |
| Old event has a new signature | Each network attempt is signed again | Dedupe by body.id and verify the current timestamp |
| Endpoint returns 200 but work is missing | Handler acknowledged before commit | Persist inbox/work atomically before acknowledgement |
| Events arrive in reverse order | Independent failures and retries | Reconcile current state and use business-operation deduplication |
Next steps
Run the seven verification programs, then follow local testing and troubleshooting.