AnnouncementsIntroducing Arkoren

    Menu

    Receiving Webhooks

    Authenticate incoming provider notifications and bind reusable event definitions to typed application handlers.

    What's on this page
    1. Mount a verified receiver and distinguish acceptance from completed business work.
    2. Define reusable sources and typed events without mixing credentials with application policy.
    3. Preserve raw request bytes, classify verification failures, and share handler bindings safely.
    On this page

    A webhook is an HTTP notification from another service. Unlike a browser request, it usually has no logged-in user: verify the provider’s credentials before acting on the notification. The webhook package authenticates each delivery, decodes the event you bind, and acknowledges application acceptance.

    Three values separate those responsibilities:

    • A source verifies requests for one provider account and returns its envelope, the provider metadata surrounding the still-encoded event payload.
    • An event associates an exact provider event key with a payload decoder. It holds no credentials and works with multiple compatible sources.
    • A receiver binds events to your application handlers and sets request policy.

    Use a provider adapter when one is shipped. Constructing an outbound mail, model, or payment driver neither creates a source nor registers an endpoint. Your application owns the public route and provider registration.

    Binding Handlers

    For Resend delivery notifications, import arkoren.dev/driver/resend/resendwebhook and arkoren.dev/core/http/webhook. Configure the endpoint in Resend and load its signing secret—not the sending API key. See the adapter setup for installation and verification.

    source, err := resendwebhook.New(signingSecret)

    Construction checks the secret’s format; verification happens on each request. Focused fragments keep construction errors visible and assume success. Handle those errors before using the returned values or registering routes.

    With context imported and an existing application service messages, accept only the delivered event:

    accept := func(ctx context.Context, delivery webhook.Delivery[resendwebhook.Email]) error {
    	return messages.RecordDelivered(ctx, delivery.Payload.EmailID)
    }

    messages.RecordDelivered is application-owned and must tolerate duplicates. Return nil only when you have accepted responsibility for the notification. Every handler error answers 503, even if some work has already committed; core does not roll back your work. A 503 permits redelivery but cannot make the provider retry. Use queued handling for slow work rather than making the provider wait.

    Bind that function while constructing the receiver:

    handler, err := webhook.Handler(source, func(receiver *webhook.Builder[resendwebhook.Envelope]) {
    	receiver.Handle(resendwebhook.EmailDelivered, accept)
    })

    The handler receives the authenticated delivery ID, exact provider event type, and decoded email metadata. It does not receive the raw request or credentials. Each key has one binding; registering it twice is an error, not fan-out.

    With your existing router r, mount the returned router.Handler directly:

    r.Post("/webhooks/resend", handler)

    After acceptance returns nil, the receiver writes 200 with an empty body. Authenticated unbound events also receive 200 without calling your handler. Acceptance is not proof that downstream work finished. Choose delivery policies before relying on redelivery, deduplication, or queue durability.

    Custom Sources

    If no adapter implements your provider’s protocol, supply a verifier with webhook.NewSource. Core supplies no signature algorithm or replay protection for custom sources. Prefer the provider’s supported verifier rather than inventing a signature format.

    For a hypothetical shipping provider in your application, define the envelope using encoding/json/jsontext:

    type Envelope struct {
    	ID   string         `json:"id"`
    	Kind string         `json:"kind"`
    	Data jsontext.Value `json:"data"`
    }

    The provider-specific verifyAndParse function below is your code, not an Arkoren helper. It must authenticate the exact received bytes, implement the provider’s timestamp/replay rules, and return an envelope only after authentication. For a known invalid request, wrap both webhook.ErrRejected and your native sentinel, for example fmt.Errorf("%w: %w", webhook.ErrRejected, ErrBadSignature). Leave transient verification failures unclassified: they answer 503 rather than rejecting a potentially genuine notification.

    With net/http imported and your account’s secret already loaded:

    source := webhook.NewSource(func(r *http.Request, body []byte) (webhook.Delivery[Envelope], error) {
    	envelope, err := verifyAndParse(r, body, secret)
    
    	if err != nil {
    		return webhook.Delivery[Envelope]{}, err
    	}
    
    	return webhook.Delivery[Envelope]{
    		ID:      envelope.ID,
    		Type:    envelope.Kind,
    		Payload: envelope,
    	}, nil
    })

    The verifier receives the bounded body exactly as sent; r.Body is already spent. Do not decode and re-encode the body before verifying a body signature. Headers, method, URL, and r.Context() remain available for protocols that need them. Do not select a business action or trust payload fields before verification.

    The receiver reads at most 1 MiB by default and rejects larger bodies with 413 before the source runs. An outer body-limit middleware can lower that ceiling. See body limits before increasing it.

    NewSource retains the function without calling it or checking credentials. Your provider constructor must validate credentials; Source.Err and Handler only detect a missing verifier. Its function, credentials, and captured clients are shared across requests and must support concurrent use.

    Typed Events

    Keep reusable event definitions separate from configured sources. For the shipping envelope above, use a small payload with the fields your handler needs:

    type Shipment struct {
    	ID       string `json:"shipment_id"`
    	Tracking string `json:"tracking_number"`
    }

    With json imported from encoding/json/v2, define the provider’s exact key:

    var ShipmentDispatched = webhook.NewEvent("shipment.dispatched", func(envelope Envelope) (Shipment, error) {
    	var shipment Shipment
    	err := json.Unmarshal(envelope.Data, &shipment)
    
    	return shipment, err
    })

    Only a bound, authenticated event reaches this decoder. Decoders must not perform network I/O; keep remote lookups in acceptance or a worker. Ordinary decoder errors answer 503, allowing a later schema fix to accept a redelivery. An authenticated decoder can return ErrSkip to acknowledge without acceptance; the skip rules differ from errors returned by handlers.

    Event keys are case-sensitive provider strings, not queue names or diagnostic categories. Empty keys, control characters, and nil decoders are invalid. Types connect a handler to its payload, not to a particular account or secret. Bind ShipmentDispatched through receiver.Handle in the same way as the shipped Resend event above.

    Shared Bindings

    A library can publish its source constructor and credential-free event definitions. To share application bindings, return a webhook.BuilderFunc rather than hiding endpoint policy inside the source. With an application-owned acceptShipment of type func(context.Context, webhook.Delivery[Shipment]) error:

    bindings := func(receiver *webhook.Builder[Envelope]) {
    	receiver.Handle(ShipmentDispatched, acceptShipment)
    }
    handler, err := webhook.Handler(source, bindings)

    Additional callbacks run once in order on the same construction draft, so the application can add receipt policy without redefining the binding. Publication copies binding and rejection containers, not functions or captured dependencies. There is no receiver lifecycle; the owners of those dependencies manage them.

    Handler reports invalid sources, events, bindings, or policy with ErrInvalid. At least one event must be bound. The first invalid setting is retained, later callbacks still run, and a supplied nil callback panics. Fix construction errors before serving requests rather than waiting for a provider delivery to reveal them.

    For every function and option, see the webhook API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive