The API reference moved here from eksis.io/developers.

Webhooks

Get an HTTPS request from eksis when a post goes out, a comment or message arrives, or a channel needs attention.

A webhook endpoint is a URL on your server that eksis calls when something happens in the workspace. You stop asking GET /v1/posts/{postId} whether a post went out, and hear about it once it did.

When to use them

Use webhooks for anything you would otherwise poll for: a post published or failed on a channel, a new comment, a new direct message, or a channel whose connection broke. Keep one periodic read of the API anyway. Delivery is at least once and can be delayed by retries, so a nightly GET /v1/posts is a cheap way to catch anything your endpoint missed while it was down.

Webhooks come with the REST API, so they need the Starter plan or higher:

PlanWebhook endpoints
FreeNone
Starter3
Premium10
Agency30

Add an endpoint

In the eksis app, open the workspace, go to API & MCP, and add an endpoint in the Webhooks section. Give it a URL, choose the events it should receive, and copy the signing secret. The secret is shown once.

You can do the same with POST /v1/webhooks. Managing webhooks needs a key with the admin role, because an endpoint keeps receiving workspace data after the key that created it is gone.

Create an endpoint
curl https://api.eksis.io/v1/webhooks \
  -H "Authorization: Bearer $EKSIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/eksis-webhooks",
    "events": ["post.target.published", "post.target.failed"],
    "description": "Publishing results"
  }'

The answer carries the endpoint and its secret, which starts with whsec_. Store it next to your other credentials.

The URL must use https and resolve to a public address. eksis refuses addresses on private networks (webhook.url_private) and does not follow redirects.

Events

An endpoint receives only the events it subscribed to. New event types are never added to an existing endpoint on their own, so a new type cannot surprise your handler.

EventSent when
post.target.publishedOne channel of a post finished publishing.
post.target.failedPublishing to one channel failed for good. Attempts that will be retried are not sent.
comment.receivedA new comment from someone else arrived.
message.receivedA direct message or a story reply arrived. data.message.kind tells them apart.
channel.unhealthyThe connection to a channel stopped working, and its posts are held.
channel.disconnectedA channel was disconnected from the workspace.
webhook.testYou pressed Send test or called the test endpoint. It cannot be subscribed to.

Which events a platform can produce is listed on its platform page.

The request

Every delivery is a POST with a JSON body:

comment.received
{
  "id": "evt_9f2c4d1a7b",
  "type": "comment.received",
  "createdAt": "2026-10-10T12:00:00.000Z",
  "workspaceId": "WORKSPACE_ID",
  "data": {
    "id": "COMMENT_ID",
    "text": "Is this available in blue?",
    "status": "open",
    "isHidden": false,
    "channelId": "CHANNEL_ID",
    "platform": "instagram",
    "postId": "POST_ID",
    "parentId": null,
    "externalPostId": "17890000000000000",
    "authorUsername": "someone",
    "authorName": "Someone",
    "createdAt": "2026-10-10T11:59:58.000Z"
  }
}

data has the same shape as the matching API resource, so code that reads GET /v1/comments can read this too. The page of each event shows its full data.

HeaderValue
webhook-idThe event id, the same as id in the body.
webhook-timestampWhen this attempt was signed, in Unix seconds.
webhook-signaturev1, followed by the base64 signature. During a secret rotation it holds two values separated by a space.
eksis-event-typeThe event type, so you can route before parsing the body.
eksis-delivery-attempt1 for the first attempt, then counting up on retries.
user-agenteksis-webhooks/1.0 (+https://docs.eksis.io/en/api/webhooks)

Verify the signature

eksis signs every request with the endpoint secret, following the Standard Webhooks format. Check the signature before you trust the body:

  1. Take the part of the secret after whsec_ and base64-decode it. That is the HMAC key.
  2. Build the string {webhook-id}.{webhook-timestamp}.{raw body}, using the body exactly as it arrived.
  3. Compute HMAC-SHA256 of that string and base64-encode it.
  4. Compare it, in constant time, with each v1, value in webhook-signature. One match is enough.
  5. Reject a webhook-timestamp more than five minutes away from your clock, so an old request cannot be replayed.

Read the raw body before any JSON parser touches it. Re-serialized JSON rarely matches the signed bytes.

import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_SECONDS = 5 * 60;

export function verifyEksisWebhook(rawBody, headers, secret) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'];
  if (!id || !timestamp || !signatures) return false;

  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;

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

  return signatures.split(' ').some((entry) => {
    const [version, value] = entry.split(',');
    if (version !== 'v1' || !value) return false;
    const received = Buffer.from(value, 'base64');
    return received.length === expected.length && timingSafeEqual(received, expected);
  });
}

The Standard Webhooks project also publishes verification libraries, among them standardwebhooks for JavaScript and for Python. They accept the eksis headers as they are.

Respond quickly

Answer with any 2xx status within 10 seconds. Anything else, a timeout, or a redirect counts as a failed attempt. If handling an event takes longer, store it, answer 2xx, and do the work afterwards.

Retries and duplicates

A failed attempt is tried again after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours, 6 hours, and 12 hours, give or take 10 percent. That is nine attempts over about 23 hours.

  • At least once. The same event can arrive more than once. Keep the webhook-id values you have handled and skip repeats.
  • No ordering. Events can arrive out of order. When order matters, compare createdAt or read the resource from the API.

An endpoint that keeps failing for 72 hours, with at least 10 failed attempts, is disabled. An endpoint that answers 410 Gone is disabled at once. The workspace owner and admins are told when this happens. Turn it back on in the app, or send PATCH /v1/webhooks/{webhookId} with "enabled": true.

Test and inspect

Send test in the app, or POST /v1/webhooks/{webhookId}/test, delivers a webhook.test event right away and shows how your endpoint answered. Test events are never retried and do not count toward disabling the endpoint.

Each endpoint keeps a log of its deliveries for 14 days: event, attempts, the status code your server returned, and how long it took. Read it in the app or with GET /v1/webhooks/{webhookId}/deliveries, and send a delivery again from the app after fixing your handler.

Rotate the secret

Rotate the secret from the app or with POST /v1/webhooks/{webhookId}/rotate-secret. The new secret is shown once. For the next 24 hours eksis signs with both the old and the new secret, so you can deploy the new one without dropping events.

Develop locally

eksis can only reach public HTTPS addresses. While you build the handler on your own machine, expose it through a tunnel such as cloudflared or ngrok, register the tunnel URL as an endpoint, and use Send test to try it.

Timing on each platform

post.target.* events are sent as soon as eksis knows the result. How fast comment.received follows a real comment depends on the platform: some platforms push new comments to eksis, others are checked on a schedule, so the event can come several minutes later. The platform specifications list how each one works.

On this page