antworkDOCS

Webhooks

Get an event POSTed to your server when a post is scheduled, published or fails, or when an account needs reconnecting, instead of polling for it.

Register an endpoint

POST /v1/webhooks
Request
curl https://antwork.io/api/v1/webhooks \
  -H "Authorization: Bearer ak_..." \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/antwork", "events": ["post.published", "post.failed"] }'
201 Created
{
  "object": "webhook",
  "id": "Hk2...",
  "workspaceId": "ws_...",
  "url": "https://example.com/antwork",
  "events": ["post.published", "post.failed"],
  "status": "active",
  "secret": "whsec_..."
}

Needs a key with the write scope. workspaceId is required when you belong to several workspaces; a webhook belongs to one. The secret signs every delivery and is shown once. At most 5 webhooks per workspace. The same webhooks are on the Developer page in the app.

  • GET /v1/webhooks?workspaceId= lists them, with consecutiveFailures, lastStatus and lastDeliveryAt.
  • PATCH /v1/webhooks/{id} changes url, events, description, or status (active | disabled).
  • DELETE /v1/webhooks/{id} removes it. Zapier and n8n subscribe and unsubscribe with this pair.
  • POST /v1/webhooks/{id}/test sends a signed ping event.
  • GET /v1/webhooks/{id}/deliveries returns the last 50 attempts, kept 30 days.
  • POST /v1/webhooks/{id}/rotate-secret returns a new secret; the old one stops signing at once.

Events

  • post.scheduled — a post was scheduled for later (a publish-now sends only its outcome).
  • post.published — a post went out; publishedUrl links to it.
  • post.failed — a post will not publish without you; error.message and error.advice say why. Sent once, after Antwork's own retries, not per attempt.
  • account.needs_reconnect — a social account's connection broke; posts on it fail until it is reconnected.
  • account.disconnected — someone disconnected a social account in Antwork.
POST to your URL
{
  "id": "evt_8f3a...",
  "type": "post.published",
  "createdAt": "2026-11-03T10:00:04.512Z",
  "workspaceId": "ws_...",
  "data": {
    "post": { "object": "post", "id": "pQ7x...", "status": "published", "publishedUrl": "https://...", ... }
  }
}

data.post is the post exactly as GET /v1/posts/{id} returns it, and data.account the account as GET /v1/accounts/{id} does, read the moment the event happened.

Verify the signature

webhook-signature

Deliveries follow the Standard Webhooks spec, so any of its libraries verifies them. The signature is HMAC-SHA256, keyed with the base64-decoded secret after whsec_, over webhook-id.webhook-timestamp.body, sent as v1,<base64> in webhook-signature. Use the raw request body, before any JSON parsing.

Node
import crypto from 'node:crypto'

function verify(rawBody, headers, secret) {
  const id = headers['webhook-id']
  const ts = headers['webhook-timestamp']
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
  const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest('base64')
  return headers['webhook-signature'].split(' ').some((s) => s === `v1,${expected}`)
}
Python
import base64, hashlib, hmac, time

def verify(raw_body: bytes, headers, secret: str) -> bool:
    msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    mac = hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest()
    expected = "v1," + base64.b64encode(mac).decode()
    return any(hmac.compare_digest(s, expected) for s in headers["webhook-signature"].split())
Refuse a webhook-timestamp more than five minutes old, so a captured delivery cannot be replayed.

Retries and switching off

  • Answer any 2xx within 10 seconds. Do the work after answering.
  • A timeout, 408, 429 or 5xx is retried with backoff from 30 seconds to an hour, 8 attempts in all, about an hour.
  • Any other answer, a redirect included, ends the event at once: redirects are never followed.
  • After 20 events in a row end undelivered, the webhook is switched off. Fix the receiver and set status back to active.
  • A retry carries the same webhook-id; use it to ignore a repeat. Events can arrive out of order.

Something look wrong? Tell us which.