FoPost.Webhooks (FoPost v0.1.0)

Copy Markdown View Source

Outbound webhooks — the push counterpart to polling a post's deliveries.

Verifying a delivery

Every delivery carries three headers:

  • X-FoPost-Signaturesha256=<hex>, the HMAC-SHA256 of the raw request body keyed with the subscription's secret
  • X-FoPost-Event — the event name
  • X-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
end

The 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.

Removes a subscription.

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.

Verifies a delivery and decodes it in one step.

Whether the X-FoPost-Signature header matches the raw body.

Functions

create(client, opts)

@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.

create!(client, opts)

Same as create/2, but raises FoPost.Error.

delete(client, id)

@spec delete(FoPost.Client.t(), String.t()) ::
  {:ok, FoPost.Message.t()} | {:error, FoPost.Error.t()}

Removes a subscription.

delete!(client, id)

Same as delete/2, but raises FoPost.Error.

events()

@spec events() :: [String.t()]

Every event a subscription can ask for.

list(client)

@spec list(FoPost.Client.t()) ::
  {:ok, [FoPost.Webhook.t()]} | {:error, FoPost.Error.t()}

The webhook subscriptions the key can reach.

list!(client)

Same as list/1, but raises FoPost.Error.

parse_event(raw_body)

@spec parse_event(binary()) ::
  {:ok, FoPost.WebhookEvent.t()} | {:error, :invalid_payload}

Decodes a delivery body into a FoPost.WebhookEvent.

signature(raw_body, secret)

@spec signature(binary(), String.t()) :: String.t()

The signature FoPost would send for this body and secret, header value and all.

Useful for testing your own handler.

test(client, id)

@spec test(FoPost.Client.t(), String.t()) ::
  {:ok, FoPost.Message.t()} | {:error, FoPost.Error.t()}

Sends a sample event to the subscribed endpoint.

test!(client, id)

Same as test/2, but raises FoPost.Error.

update(client, id, opts)

@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.

update!(client, id, opts)

Same as update/3, but raises FoPost.Error.

verify_and_parse(raw_body, header, secret)

@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}.

verify_signature(raw_body, header, secret)

@spec verify_signature(binary(), String.t() | nil, String.t()) :: boolean()

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.