Outbound webhooks — the push counterpart to polling a post's deliveries.
Verifying a delivery
Every delivery carries three headers:
X-FoPost-Signature—sha256=<hex>, the HMAC-SHA256 of the raw request body keyed with the subscription's secretX-FoPost-Event— the event nameX-FoPost-Delivery— the delivery's own id
Verify against the raw body, before any JSON decoding, or the bytes will not match:
def handle(conn) do
{:ok, raw, conn} = Plug.Conn.read_body(conn)
[signature] = Plug.Conn.get_req_header(conn, "x-fopost-signature")
case FoPost.Webhooks.verify_and_parse(raw, signature, secret) do
{:ok, event} -> process(event)
{:error, :invalid_signature} -> Plug.Conn.send_resp(conn, 401, "")
end
endThe comparison is constant time. No timestamp is mixed into the signature, so there is
no replay window to enforce — deduplicate on X-FoPost-Delivery if you need it.
Summary
Functions
Subscribes an endpoint to a workspace's events.
Same as create/2, but raises FoPost.Error.
Removes a subscription.
Same as delete/2, but raises FoPost.Error.
Every event a subscription can ask for.
The webhook subscriptions the key can reach.
Same as list/1, but raises FoPost.Error.
Decodes a delivery body into a FoPost.WebhookEvent.
The signature FoPost would send for this body and secret, header value and all.
Sends a sample event to the subscribed endpoint.
Same as test/2, but raises FoPost.Error.
Changes a subscription's endpoint, events, or active flag.
Same as update/3, but raises FoPost.Error.
Verifies a delivery and decodes it in one step.
Whether the X-FoPost-Signature header matches the raw body.
Functions
@spec create( FoPost.Client.t(), keyword() ) :: {:ok, FoPost.Webhook.t()} | {:error, FoPost.Error.t()}
Subscribes an endpoint to a workspace's events.
Required: :workspace_id, :url, :events. The signing secret comes back on
:secret, once and only here — store it now.
Same as create/2, but raises FoPost.Error.
@spec delete(FoPost.Client.t(), String.t()) :: {:ok, FoPost.Message.t()} | {:error, FoPost.Error.t()}
Removes a subscription.
Same as delete/2, but raises FoPost.Error.
@spec events() :: [String.t()]
Every event a subscription can ask for.
@spec list(FoPost.Client.t()) :: {:ok, [FoPost.Webhook.t()]} | {:error, FoPost.Error.t()}
The webhook subscriptions the key can reach.
Same as list/1, but raises FoPost.Error.
@spec parse_event(binary()) :: {:ok, FoPost.WebhookEvent.t()} | {:error, :invalid_payload}
Decodes a delivery body into a FoPost.WebhookEvent.
The signature FoPost would send for this body and secret, header value and all.
Useful for testing your own handler.
@spec test(FoPost.Client.t(), String.t()) :: {:ok, FoPost.Message.t()} | {:error, FoPost.Error.t()}
Sends a sample event to the subscribed endpoint.
Same as test/2, but raises FoPost.Error.
@spec update(FoPost.Client.t(), String.t(), keyword()) :: {:ok, FoPost.Webhook.t()} | {:error, FoPost.Error.t()}
Changes a subscription's endpoint, events, or active flag.
Same as update/3, but raises FoPost.Error.
@spec verify_and_parse(binary(), String.t() | nil, String.t()) :: {:ok, FoPost.WebhookEvent.t()} | {:error, :invalid_signature | :invalid_payload}
Verifies a delivery and decodes it in one step.
Answers {:ok, event}, {:error, :invalid_signature}, or {:error, :invalid_payload}.
Whether the X-FoPost-Signature header matches the raw body.
Pass the body exactly as it arrived, before any decoding. The comparison is constant time, so a wrong signature reveals nothing about how wrong it was.