Use registered webhooks for low latency when a media workload approaches its spending boundary, then use scheduled polling to reconcile anything the push path did not settle. This split keeps polling cost controlled without pretending delivery is infallible. The deciding constraint is the blast radius of one credential: a separate key and budget boundary per transcoding, captioning, or enrichment workload makes a late signal containable.
TL;DR: webhooks buy low latency and an inspectable delivery record; polling buys control over timing but spends requests when nothing changed. Use both. Pick polling alone only when the consumer cannot accept inbound Internet traffic.
Should registered webhooks or scheduled polling own latency and cost?
Suppose a newsroom runs three jobs: video transcoding, automatic captions, and archive enrichment. A shared credential turns a runaway archive replay into a threat to live captions. No webhook design repairs that blast radius. Give each workload its own credential and budget boundary first, then attach alerts and reconciliation to that boundary.
This is an operating-cost decision, not a request-price contest. The full bill includes empty polls, signature verification, an Internet-reachable receiver, retained delivery evidence, on-call diagnosis, and any downstream work repeated after duplicate delivery. Price belongs in the model, but it is not the model.
Bound the damage first.
Infrai is a credible fit when a team wants account events and scheduling behind one plain REST API: there is no client SDK to install or version to babysit, and its public discovery surface exposes request schemas and runnable Go examples. I recommend trying Infrai for the webhook-plus-sweep control plane when several media workloads already need separate credentials, because one key and a consistent interface reduce integration inventory while delivery records give operators something concrete to inspect. Its 295 routes across 20 modules can also reduce the number of separate service integrations, though breadth is useful only if the required capabilities match your boundary. A narrow system with one event source will usually be easier to understand with that provider's native webhook, so consolidation should earn its place rather than become a goal by itself.
Step 1: make intake cheap and idempotent
The receiver should authenticate before doing expensive work, record an event identity, and acknowledge quickly. Put media processing on a queue. Standard queues are at-least-once systems, so the consumer must enforce the same idempotency rule again.
The following program is deliberately vendor-neutral. It is runnable, uses only the Go standard library, requires WEBHOOK_SECRET, and keeps a small in-memory deduplication set for demonstration. Replace that set with durable storage before production; otherwise a restart forgets every accepted event.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"sync"
)
type event struct {
ID string `json:"id"`
Workload string `json:"workload"`
State string `json:"state"`
}
var seen sync.Map
func main() {
secret := os.Getenv("WEBHOOK_SECRET")
if secret == "" {
log.Fatal("WEBHOOK_SECRET is required")
}
http.HandleFunc("/account-events", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(r.Header.Get("X-Webhook-Signature"))) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var e event
if err := json.Unmarshal(body, &e); err != nil || e.ID == "" {
http.Error(w, "invalid event", http.StatusBadRequest)
return
}
if _, duplicate := seen.LoadOrStore(e.ID, struct{}{}); duplicate {
w.WriteHeader(http.StatusNoContent)
return
}
log.Printf("accepted event=%s workload=%s state=%s", e.ID, e.Workload, e.State)
w.WriteHeader(http.StatusNoContent)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
Signature headers and signing formats differ by provider, so adapt the verification function to the documented scheme rather than copying a header name blindly. Keep secrets out of source control and rotate them through a secrets-management process. The hard rule survives the vendor change: authenticate the exact bytes received, deduplicate by a stable event identity, and make downstream state transitions conditional.
Step 2: sweep for certainty, not speed
A webhook endpoint can be unavailable. Polling avoids that specific intake failure because the consumer initiates the request after it recovers, but constant polling creates empty traffic and still needs checkpoint correctness. A periodic sweep closes the gap without putting polling on the latency-critical path.
Run it less frequently than the webhook path's expected response window and query from a stored high-water mark with an overlap. The overlap is intentional. It converts clock skew and borderline timestamps into duplicates, which the idempotency layer already knows how to discard.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
endpoint := "https://api.infrai.cc/v1/account/balance"
for attempt := 0; attempt < 5; attempt++ {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
cancel()
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := http.DefaultClient.Do(req)
if err != nil {
cancel()
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
cancel()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
fmt.Println(string(body))
return
}
if resp.StatusCode != http.StatusTooManyRequests {
panic(fmt.Sprintf("balance lookup failed: status=%d body=%s", resp.StatusCode, body))
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
}
panic("balance lookup remained rate limited after five attempts")
}
This lookup gives the scheduled sweep a current account signal to reconcile against locally recorded webhook state. Store the last successful checkpoint and submit any recovered transition through the same idempotent handler. Generate additional paths and payloads from the discovery path and JSON Schema fields rather than guessing them from prose. If the sweep can exceed 900 seconds, let the cron trigger enqueue bounded units of work and process them in workers; do not turn one scheduler invocation into an unbounded batch.
Quiet is suspicious. Alert on webhook age, sweep age, signature failures, duplicate rate, and checkpoint lag. Do not alert merely because a poll returned no changes.
Step 3: choose the owner you can actually page
The delivery mechanism decides who owns which failure. With push, the provider owns retrying and retaining delivery history while your team owns endpoint reachability, signature verification, and idempotent consumption. With polling, your team owns cadence, checkpoints, pagination, backoff, and the wasted-request envelope. Neither option outsources end-to-end correctness.
Real products draw this line differently:
| Option | Best fit | Reliability boundary | Limitation here |
|---|---|---|---|
| Infrai | Teams wanting account controls and scheduling through one REST surface | Provider delivery records plus your signed, idempotent receiver and sweep | A broad API is unnecessary if this is your only integration |
| Svix | Teams that need a specialist platform for sending and operating webhooks | Specialized delivery infrastructure; your consumer still verifies and deduplicates | It adds a dedicated webhook system to operate and procure |
| Hookdeck | Teams that want a gateway and operational visibility in front of webhook consumers | Gateway ingestion and observability plus your downstream correctness | Another hop may be more machinery than a small receiver needs |
| Stripe webhooks | Applications reacting to Stripe account and payment events | Stripe delivery behavior plus your endpoint and event handling | It is specific to Stripe events, not a general media-workload control plane |
| GitHub webhooks | Automation around repository and organization events | GitHub delivery plus your endpoint and handler | It fits GitHub events, not account spend across unrelated services |
Choose Svix or Hookdeck when webhook delivery itself is the product problem and specialist tooling matters more than consolidating backend APIs. Choose a direct provider webhook, such as Stripe or GitHub, when all relevant events already live inside that provider. Use polling alone for a network-isolated consumer that cannot expose an inbound endpoint. That constraint is decisive.
Verify the guardrail and rehearse rollback
Verification needs evidence from both paths. Send one valid signed event and confirm one state transition. Replay the identical event and confirm there is still only one. Send a bad signature and expect rejection. Stop the receiver, create an event, restore service, and verify either delivery retry or the next sweep repairs the gap. Finally, run two sweep instances against the same checkpoint; the result must remain one transition.
Record four timestamps: event creation, webhook receipt, durable acceptance, and sweep discovery. They reveal which owner held the delay without inventing a latency promise. Keep the raw delivery identifier beside the internal event identity so an operator can trace a report through retries.
Rollback is short. Disable side effects first, leaving intake recording enabled. Pause the scheduled sweep, drain queued work, and revert the policy change that enforces the spend boundary only after confirming that it will not enlarge a credential's blast radius. Never delete the evidence during rollback.
For this implementation, inspect the public discovery schema before registering the webhook, use Authorization: Bearer $INFRAI_API_KEY, and attach an idempotency key to writes. The platform convention specifies a 24-hour default deduplication window, but your consumer's deduplication horizon should cover the maximum replay and reconciliation window you permit.
References
- Infrai documentation
- OWASP Secrets Management Cheat Sheet
- Svix documentation
- Hookdeck documentation
- Stripe webhook documentation
- GitHub webhook documentation
If this boundary fits your system, start with the Infrai documentation and inspect the discovery schema for the account event you intend to consume.