automation and the api

Webhooks, with an end-to-end test

Send a signed message to another tool when a post goes live, fails or waits for approval, and check it arrived.

The events

post.published: a post went live on a network. post.failed: a post failed to publish. post.awaiting_approval: a post is waiting for somebody to approve it. connection.expired: a connected account stopped working and needs reconnecting.

Every delivery is a JSON body holding id, type, created_at, workspace_id and data, signed with three headers: webhook-id, webhook-timestamp and webhook-signature. The signature follows the Standard Webhooks scheme: an HMAC-SHA256 of the id, the timestamp and the raw body under your secret.

Test it end to end in five minutes

  1. Open a request inspector such as webhook.site in another tab. It gives you a unique https:// address and shows every request sent to it.
  2. Open Settings, then API access, then Webhooks, and add an endpoint. Paste the inspector's address, which has to start with https://. Press Edit events on the endpoint to choose which events it receives.
  3. Copy the signing secret. It starts with whsec_ and is shown only once.
  4. Choose an event under Send test and press Send test. A test has the same type and data keys as a real event, with every id and link null and test set to true.
  5. In the inspector, check the request arrived with the three webhook headers and the JSON body. On API access, the delivery appears under Recent deliveries.
  6. Copy the raw body and the three headers from the inspector and run them through the check below with your secret. It returns true when the signature is genuine. A test is signed like any other delivery, so the check has to run within five minutes of sending it.

Checking the signature in Node

Check the signature before trusting anything in the body, and check it against the raw body exactly as it arrived: a body parsed into JSON and serialised again will not match.

const { createHmac, timingSafeEqual } = require('node:crypto')

// secret: the whsec_ value shown once when you added the endpoint
// headers: the request headers, lowercased
// rawBody: the body exactly as it arrived, before any JSON parsing
function verify(secret, headers, rawBody) {
  const id = headers['webhook-id']
  const timestamp = headers['webhook-timestamp']
  const signature = headers['webhook-signature'] || ''

  // Refuse anything older or newer than five minutes, so a captured request cannot be replayed.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false

  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
  const expected = Buffer.from(
    createHmac('sha256', key).update(id + '.' + timestamp + '.' + rawBody).digest('base64')
  )

  return signature.split(' ').some((part) => {
    const [version, value] = part.split(',')
    const given = Buffer.from(value || '')
    return version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected)
  })
}

Delivery and retries

Your endpoint has ten seconds to answer with a 2xx status. A delivery that fails is retried after 5 minutes, 15 minutes, 1 hour, 4 hours and 12 hours, then given up on, and Recent deliveries says so. Deliveries are kept there for 30 days.

The same event can arrive twice, for example when your server accepted it but answered too slowly. Use the id in the body to ignore a repeat.

In Zapier, paste the address of a Catch Hook. In Make, use a custom webhook. In n8n, a Webhook node. Webhooks are managed by owners and admins.