Skip to content

Webhooks

Subscriptions that POST draft events to your URL, and what each delivery carries.

6 endpoints

GET/webhooks

Webhook subscriptions

Any key

Webhooks receive draft events from every group, so these routes need a key with access to all groups (403 forbidden otherwise). They also spend the webhook-management budget Settings uses (30 a minute).

Returns

200 with these fields; an error answers in the Error envelope.

curl https://postacio.com/api/v1/webhooks \
  -H "Authorization: Bearer $POSTACIO_API_KEY"
POST/webhooks

Subscribe a URL to draft events

Any keyIdempotency-Key

The answer carries the whsec_ signing secret, once. Up to 10 webhooks (409 after); a private, local or plain-http URL is 422 on url. Each event is POSTed as JSON (WebhookPayload, at most 32 KiB) with Postacio-Event, Postacio-Delivery-Id and Postacio-Signature: t=<unix seconds>,v1=<hex hmac-sha256(secret, "<t>.<raw body>")>. Verify against the raw body and reject a t more than 5 minutes off. Answer 2xx within 30 s; failures are retried 7 times with backoff, under the same Postacio-Delivery-Id, and a webhook that keeps failing (25 attempts over at least 24 hours) is paused until resumed.

Webhooks receive draft events from every group, so these routes need a key with access to all groups (403 forbidden otherwise). They also spend the webhook-management budget Settings uses (30 a minute).

Headers

  • Idempotency-Keystring

    Up to 128 visible ASCII characters. Retrying with the same key and body within 24 hours replays the first answer (with Idempotent-Replayed: true) instead of doing the work twice; the same key with a different body is 422 idempotency_conflict.

    up to 128 characters

Body

  • urlstringrequired

    A public https:// address. Private, local and plain-http addresses are refused (422).

    1 to 2048 characters

  • eventsarray of stringsrequired

    The draft events to send.

    1 to 9 items

    draft.createddraft.updateddraft.planneddraft.scheduleddraft.unscheduleddraft.publisheddraft.partially_faileddraft.faileddraft.deleted
  • descriptionstring or null

    A note for you, shown in Settings.

    up to 120 characters

Returns

201 with a CreatedWebhook object; an error answers in the Error envelope.

curl -X POST https://postacio.com/api/v1/webhooks \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://hooks.example.com/postacio",
    "events": [
      "draft.published",
      "draft.failed"
    ]
  }'
GET/webhooks/{webhookId}

One webhook

Any key

Webhooks receive draft events from every group, so these routes need a key with access to all groups (403 forbidden otherwise). They also spend the webhook-management budget Settings uses (30 a minute).

Path

  • webhookIduuidrequired

    A webhook id from GET /webhooks.

Returns

200 with a Webhook object; an error answers in the Error envelope.

curl https://postacio.com/api/v1/webhooks/$WEBHOOK_ID \
  -H "Authorization: Bearer $POSTACIO_API_KEY"
PATCH/webhooks/{webhookId}

Change a webhook's URL, events or description, or resume it

Any key

status: "active" resumes a paused webhook. Webhooks receive draft events from every group, so these routes need a key with access to all groups (403 forbidden otherwise). They also spend the webhook-management budget Settings uses (30 a minute).

Path

  • webhookIduuidrequired

    A webhook id from GET /webhooks.

Body

  • urlstring

    1 to 2048 characters

  • eventsarray of strings

    1 to 9 items

    draft.createddraft.updateddraft.planneddraft.scheduleddraft.unscheduleddraft.publisheddraft.partially_faileddraft.faileddraft.deleted
  • descriptionstring or null

    up to 120 characters

  • status"active"

    "active" resumes a paused webhook and clears its failure streak. Pausing is automatic only.

Returns

200 with a Webhook object; an error answers in the Error envelope.

curl -X PATCH https://postacio.com/api/v1/webhooks/$WEBHOOK_ID \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "active"
  }'
DELETE/webhooks/{webhookId}

Unsubscribe: delete a webhook and its delivery history

Any key

Webhooks receive draft events from every group, so these routes need a key with access to all groups (403 forbidden otherwise). They also spend the webhook-management budget Settings uses (30 a minute).

Path

  • webhookIduuidrequired

    A webhook id from GET /webhooks.

Returns

204 with no body; an error answers in the Error envelope.

curl -X DELETE https://postacio.com/api/v1/webhooks/$WEBHOOK_ID \
  -H "Authorization: Bearer $POSTACIO_API_KEY"
POSTYour webhook URL

A draft event, sent to each subscribed webhook URL

Sent by Postacio

Each event is POSTed as JSON (WebhookPayload, at most 32 KiB) with Postacio-Event, Postacio-Delivery-Id and Postacio-Signature: t=<unix seconds>,v1=<hex hmac-sha256(secret, "<t>.<raw body>")>. Verify against the raw body and reject a t more than 5 minutes off. Answer 2xx within 30 s; failures are retried 7 times with backoff, under the same Postacio-Delivery-Id, and a webhook that keeps failing (25 attempts over at least 24 hours) is paused until resumed.

Headers

  • Postacio-Eventstringrequired
  • Postacio-Delivery-Iduuidrequired
  • Postacio-Signaturestringrequired

    matches ^t=\d+,v1=[0-9a-f]{64}$

Payload

  • iduuid

    The delivery id, the same as the Postacio-Delivery-Id header. De-duplicate on it.

  • eventstring
    draft.createddraft.updateddraft.planneddraft.scheduleddraft.unscheduleddraft.publisheddraft.partially_faileddraft.faileddraft.deleted
  • created_atdatetime
  • originobject
    Show 2 fields
    • kindstring
      appapiagent
    • namestring or null
  • dataobject
    Show 2 fields
    • draftobject

      The draft as GET /drafts/{id} returns it; only { id, group_id } for draft.deleted or when truncated.

      Shape 1: 24 fields
      • iduuid
      • group_iduuid or null
      • statusstring
        draftplannedscheduledheldpublishingpublishedpartially_failedfailed
      • publish_statestring
        idlein_progressfinished
      • kindstring

        What the draft is; fixed once created.

        postarticlereplyquotepollreelstoryrepostcomment
      • textstring
      • threadarray of strings
      • pollobject or null

        The poll on the first post, for a poll draft.

        Show 5 fields
        • optionsarray of strings
        • duration_daysinteger

          -9007199254740991 to 9007199254740991

        • multiple_answersboolean
        • quizobject
          Show 2 fields
          • correct_optioninteger

            -9007199254740991 to 9007199254740991

          • explanationstring
        • close_after_daysinteger

          -9007199254740991 to 9007199254740991

      • target_urlstring or null

        The post a reply, quote, repost or comment acts on.

      • titlestring or null
      • tagsarray of strings
      • planned_atdatetime or null
      • scheduled_atdatetime or null
      • published_atdatetime or null
      • share_urlstring or null
      • originobject
        Show 2 fields
        • kindstring
          appapiagent
        • namestring or null
      • versioninteger

        -9007199254740991 to 9007199254740991

      • created_atdatetime
      • updated_atdatetime
      • targetsarray of objects
        Show 7 fields
        • account_iduuid or null
        • platformstring
          xtelegraminstagramfacebooklinkedintiktokyoutube
        • statusstring or null
          pendingpublishingpublishedfailed
        • textstring or null
        • permalinkstring or null
        • errorstring or null
        • published_atdatetime or null
      • warningsarray of objects

        "needs_person": an API key or AI agent wrote this reply to, or quote of, someone else's X post. It does not publish until a person approves it in Postacio; a key or agent cannot.

        Show 2 fields
        • codestring
        • messagestring
      • approvalobject

        Where the draft stands with its group's approval gate.

        Show 6 fields
        • requiredboolean

          The group asks for approval before a post is scheduled or published.

        • statusstring
          not_requestedpendingchanges_requestedapproved
        • approved_versioninteger or null

          The version the approval covers.

          -9007199254740991 to 9007199254740991

        • currentboolean

          Nothing stands in the way: approval isn't required, or it covers this version.

        • clientobject

          The client's half of the gate (approval views).

          Show 4 fields
          • requiredboolean

            The group needs a reviewer's approval before a post is scheduled or published.

          • statusstring

            Where the post stands with its client.

            not_sentsentin_reviewapprovedexempt
          • approved_versioninteger or null

            The version the client approved (or that an exemption covers).

            -9007199254740991 to 9007199254740991

          • currentboolean

            The client's approval isn't required, or it covers this version.

        • missingarray of strings

          Which approvals still stand between this post and going out. Empty when nothing does.

          teamclient
      • hold_reasonstring or null

        Why a "held" post is waiting: "awaiting_approval"; "needs_person" (an API key or AI agent wrote this reply to or quote of someone else's X post, and a person must approve it in Postacio); "limit" (the monthly allowance); "paused_group" or "paused_account".

      Shape 2: 2 fields
      • iduuid
      • group_iduuid or null
    • truncatedtrue

      The full draft was over 32 KiB; fetch it through the API.

{
  "id": "8c1d0f3e-5b2a-4e7c-9a61-2f4b7d9e0c13",
  "event": "draft.created",
  "created_at": "2026-10-14T09:00:00.000Z",
  "origin": {
    "kind": "app",
    "name": "Newsletter bot"
  },
  "data": {
    "draft": {
      "id": "8c1d0f3e-5b2a-4e7c-9a61-2f4b7d9e0c13",
      "group_id": "8c1d0f3e-5b2a-4e7c-9a61-2f4b7d9e0c13",
      "status": "draft",
      "publish_state": "idle",
      "kind": "post",
      "text": "Hello from the Postacio API",
      "thread": [
        "…"
      ],
      "poll": {
        "options": [
          "…"
        ],
        "duration_days": -9007199254740991,
        "multiple_answers": false,
        "quiz": {
          "correct_option": -9007199254740991,
          "explanation": "…"
        },
        "close_after_days": -9007199254740991
      },
      "target_url": "https://example.com/",
      "title": "Launch week",
      "tags": [
        "…"
      ],
      "media": [
        {
          "key": "…",
          "kind": "image",
          "content_type": "image/png",
          "name": "Newsletter bot",
          "size_bytes": 1,
          "duration_sec": 1,
          "alt_text": "…"
        }
      ],
      "planned_at": "2026-10-14T09:00:00.000Z",
      "scheduled_at": "2026-10-14T09:00:00.000Z",
      "published_at": "2026-10-14T09:00:00.000Z",
      "share_url": "https://example.com/",
      "origin": {
        "kind": "app",
        "name": "Newsletter bot"
      },
      "version": -9007199254740991,
      "created_at": "2026-10-14T09:00:00.000Z",
      "updated_at": "2026-10-14T09:00:00.000Z",
      "targets": [
        {
          "account_id": "8c1d0f3e-5b2a-4e7c-9a61-2f4b7d9e0c13",
          "platform": "x",
          "status": "pending",
          "text": "Hello from the Postacio API",
          "permalink": "https://x.com/juniper/status/1846000000000000000",
          "error": "…",
          "published_at": "2026-10-14T09:00:00.000Z"
        }
      ],
      "warnings": [
        {
          "code": "…",
          "message": "A sentence written for a person."
        }
      ],
      "approval": {
        "required": false,
        "status": "not_requested",
        "approved_version": -9007199254740991,
        "current": false,
        "client": {
          "required": false,
          "status": "not_sent",
          "approved_version": -9007199254740991,
          "current": false
        },
        "missing": [
          "team"
        ]
      },
      "hold_reason": "…"
    },
    "truncated": true
  }
}

Ready to send your first draft?

Start your free trial, create a key in Settings, and call the API in a minute.