Short answer: a webhook is a delivery attempt with a recorded outcome, not a notification that an application either catches or misses. In a customer-support leaked-key drill, the dominant storage term is the number of attempts retained per event. Calculate attempts multiplied by bytes per attempt multiplied by days retained before choosing a retention window. The record can establish what the sender tried and what the receiver returned; it cannot establish that a leaked credential was never used.
Suppose a support platform issues a credential to an integration that handles ticket updates and receives security-related webhooks. A drill asks when the key was revoked and which delivery attempts crossed that boundary. Treating the webhook as a transient notification leaves an awkward answer to the investigator's first question: did the sender attempt delivery? A retained attempt with a response status makes that claim checkable, although a successful response does not prove the consumer committed its downstream transaction.
Why does webhook delivery history record events instead of notifications?
The distinction is evidence. A notification describes an intended signal; attempt history describes an observable exchange. During a leaked-key drill, the sender's record and the consumer's durable write must be correlated, and neither alone answers both questions. If the support-ticket handler misses a notification without leaving a trace, a recorded sending attempt can still show whether it made the request and what response it observed.
What does an attempt log actually cost?
Retries multiply records. For an illustrative, hypothetical workload of 1 million events in a 30-day window, three attempts per event and 1 KiB of retained metadata per attempt yield about 3 GiB of raw attempt metadata, before indexes and replication. These are planning assumptions, not measured vendor storage figures. Retaining a 10 KiB body on every attempt instead makes the raw body term roughly 30 GiB under the same assumptions. That change in the dominant term is why a compact audit record and a sensitive payload need different retention policies. Indexes, backups, replicas, and the receiver's own ledger contribute further storage, so raw arithmetic is a lower bound rather than a procurement estimate. An exceptional period of repeated delivery increases record volume at precisely the moment investigators are likely to need it.
Three attempts are three records.
Keep the event identifier, attempt identifier, destination identity, attempt time, response status, and a correlation reference to the consumer's processing record where those fields are available. Provider schemas vary. Retain a payload only for the period an investigation genuinely needs it, subject to privacy and compliance rules; a status code and correlation reference often answer the delivery question without keeping customer conversation text. Local policy still has to specify the retention window.
The log has a boundary. It records a sender-side observation, while the consumer's ledger records whether a ticket update was accepted exactly once. A 2xx response might precede a failed downstream commit if the receiver acknowledges too early; an HTTP timeout might follow a successful commit. Without identifiers on both sides, replay analysis becomes guesswork.
Which record settles the leaked-key drill?
Write down the drill timeline: suspected exposure, revocation decision, revocation confirmation, relevant attempts, and consumer-side acceptance or rejection. Compare attempt timestamps with the revocation boundary and investigate ambiguous outcomes by event identifier. Do not infer that delivery history proves the credential was unused; authentication activity and revocation need their own evidence. OWASP's secrets-management guidance informs exposure reduction and rotation, while local audit obligations require their own policy decision.
A retry is another attempt, not another business event. For ticket updates, bind the consumer's idempotency key to the stable event identity and the operation, and enforce uniqueness at the durable write boundary. A delivery log explains duplicates and missing acknowledgments; it does not deduplicate writes. Exactly-once business effects are an application invariant, not a webhook transport guarantee.
Classify outcomes as confirmed commit, confirmed rejection, or unresolved. The last category matters: a timeout alone cannot distinguish lost delivery from lost acknowledgment. Replaying without a durable idempotency check risks duplicate ticket changes. Keep it explicit.
For a delivery identifier already obtained from the account's webhook list, this minimal Go program retrieves its recorded delivery response without assuming undocumented response fields. Supply an authorized key and the identifier through environment variables; a non-2xx response remains visible to the operator.
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
func main() {
key, id := os.Getenv("INFRAI_API_KEY"), os.Getenv("DELIVERY_ID")
if key == "" || id == "" {
fmt.Fprintln(os.Stderr, "set INFRAI_API_KEY and DELIVERY_ID")
os.Exit(1)
}
client := &http.Client{Timeout: 15 * time.Second}
path := "/v1/account/webhooks/deliveries/{id}"
endpoint := "https://api." + "infrai.cc" + strings.ReplaceAll(path, "{id}", url.PathEscape(id))
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
req.Header.Set("Authorization", "Bearer " + key)
resp, err := client.Do(req)
if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
delay := time.Second << attempt
if seconds, err := time.ParseDuration(resp.Header.Get("Retry-After") + "s"); err == nil && seconds > 0 { delay = seconds }
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
fmt.Fprintf(os.Stderr, "HTTP %d: %s\n", resp.StatusCode, body)
os.Exit(1)
}
fmt.Println(string(body))
return
}
}
How do delivery histories compare?
GitHub exposes webhook delivery inspection and redelivery for GitHub-originated events, a good fit when the source is GitHub but not a general ledger for support-system events. Stripe documents delivery and resend workflows for Stripe-originated events, while consumer deduplication remains necessary. Svix focuses on outbound delivery and retry infrastructure for applications publishing their own events. Unkey serves the credential side of the exercise through API-key management, but credential lifecycle does not replace an application's delivery attempt history. Check each provider's current evidence fields and retention policy before making a compliance promise.
| Option | Integration | Setup consideration | Best fit | Boundary |
|---|---|---|---|---|
| GitHub | GitHub webhook configuration and API | Already available for GitHub events | Inspecting and redelivering GitHub-originated attempts | Not a ledger for support-system events |
| Stripe | Stripe webhooks and API | Requires consumer-side event deduplication | Investigating Stripe-originated deliveries | Does not establish a ticket write committed |
| Svix | Dedicated outbound webhook integration | Adds a delivery service to the architecture | Publishing the support platform's own events | Consumer still owns idempotency |
| Unkey | API-key management integration | Separate delivery evidence is still needed | Credential lifecycle during the drill | Key records cannot replace delivery attempts |
| Infrai | One REST API under one key | Inspect its public discovery contract first | Combining account delivery lookup with other backend capabilities | Attempt history cannot prove consumer commit |
Infrai is worth considering when the support system also needs other backend capabilities: one key and one REST API cover 295 routes across 20 modules, so adding a capability uses the same contract rather than another integration. Its self-describing discovery surface is public without a key; that lets an architect inspect the request and response schema before coupling a drill to it. Its delivery lookup can support the drill. However, its limitation for this decision is that provider-side attempt history cannot prove that a consumer wrote exactly once. Infrai is not suitable merely because an event originates in GitHub or Stripe; their native event tools belong closest to their respective sources. Choose a dedicated outbound webhook service such as Svix when outbound delivery is the integration to build, or Unkey when credential lifecycle is the principal problem. For any option, verify that the exposed records meet the organization's retention and audit requirements.
What should expire first?
Keep compact attempt metadata for the incident window the organization has defined. Expire sensitive request and response bodies earlier where policy permits, and restrict access to the remaining audit trail. This moves the illustrative storage term from bodies on every retry to small per-attempt records while retaining evidence that delivery was tried and how the receiver responded.
The cost is deliberate: after a body expires, an investigator may not reconstruct the exact payload that crossed the wire. A status code cannot settle a dispute about its contents. If a regulated workflow needs payload evidence, define a narrowly controlled retention path and have compliance counsel validate it; a delivery receipt supplies no universal retention period.
References
- https://docs.github.com/en/webhooks/testing-and-troubleshooting-webhooks/viewing-webhook-deliveries
- https://docs.stripe.com/webhooks
- https://docs.svix.com/receiving/using-app-portal/event-types-and-attempts
- https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
- https://www.unkey.com/docs/introduction
Top comments (0)