# Guide to Ayrshare Webhooks: Features and Best Practices

Ayrshare lets an application publish and manage posts across social networks through one API, often on behalf of many end users who each have their own User Profile. Most of what Ayrshare does finishes later than the API call that started it: a scheduled post goes out at 9am, TikTok takes a while to publish a video, a follower replies with a comment or a direct message. Ayrshare webhooks report those results to your server as they happen.

> Register a webhook secret with Ayrshare. Without one, deliveries are unsigned and fail verification.

This guide explains Ayrshare's per-action, per-profile webhook model, the payload fields that matter, how to set and rotate the profile-wide signing secret, how to verify the V2 signature header, and how Ayrshare's retry rules affect your status codes.

## What are Ayrshare webhooks?

Ayrshare registers webhooks by **action**, such as `scheduled`, `messages` or `comments`, and by profile. A User Profile's own webhook for an action is called first. If the User Profile has none, Ayrshare calls the Primary Profile's webhook for that action, so one registration on the Primary Profile can serve every user. The URL must be HTTPS and must not redirect.

Each delivery is a JSON `POST`. Signing is opt-in: until you set a secret, requests carry no signature. Once a secret is set, Ayrshare adds an HMAC-SHA256 of the raw body in two headers, a legacy one with the bare hex digest and a V2 one that can hold two signatures during a secret rotation.

## Ayrshare webhook features

| Feature | Details |
| --- | --- |
| Registration | `POST /hook/webhook` per action, or the Developer Dashboard |
| Scope | Primary Profile, or a User Profile that overrides it |
| Signature | hex HMAC-SHA256 |
| Signature header | `x-authorization-content-sha256-v2` |
| Signature format | `v1=<hex>`, or `v1=<current>,v1=<previous>` for 24 hours after a rotation |
| Legacy header | `X-Authorization-Content-SHA256`, bare hex of the current signature |
| Timestamp header | `X-Authorization-Timestamp`, Unix seconds, not signed |
| Delivery headers | `X-Ayrshare-Delivery-Id` (per attempt), `X-Ayrshare-Delivery-Attempt` (`0` first) |
| Secret | Optional, one per User Profile, shared by all its actions |
| Body | JSON |
| Event routing | Body field `action` |
| Response window | `200` within 15 seconds |
| Retries | `408`, `425`, `429`, `5xx`, timeouts and dropped connections, for up to about 16 hours |

Ayrshare publishes no source IP list for webhooks.

## Events and payloads

| `action` | Refined by | Triggered when |
| --- | --- | --- |
| `scheduled` | `subAction: tikTokPublished` | A scheduled post is processed (not sent for immediate posts); TikTok finishes publishing |
| `social` | `type`: `link`, `unlink`, `refresh` | A social account is linked, unlinked or must be refreshed |
| `messages` | `subAction`: `messageCreated`, `messageEdited`, `messageRead`, `reactionCreated` | A message arrives, is edited, read or reacted to |
| `batch` | `type: batch`, `batchType` | A batch export is ready (`url` expires at `urlExpires`) |
| `feed` |  | A new item appears in a registered RSS feed |
| `mentions` | `subAction: mention` | Your Instagram account is mentioned |
| `comments` | `subAction: comment` | A comment is created on connected content (Facebook, Instagram, Threads) |
| `automations` | `type`: `automation.triggered`, `automation.completed` | An automation fires or finishes |
| `demo` | `type`: `upgradeWarning`, `upgrade` | Demo plan limit events |
| `suspended` | `type`: `suspend`, `unsuspend` | A profile is suspended or reinstated |
| `ads` | `type`: `created`, `spendSynced`, `accountConnected`, `accountDisconnected`, `accountRevoked` | Ad or ad account events |

Not every payload carries a `subAction` (the documented `messages` example has none), so treat it as optional. Ayrshare shows `subAction` values in examples and prose rather than listing them per action.

| Field | Meaning |
| --- | --- |
| `action` | The registration category |
| `hookId` | Identifies one delivery of an event; identical on every retry. Dedupe on it |
| `refId` | User Reference ID of the profile |
| `url` | Your webhook URL |
| `created` | ISO 8601 time of the event |
| `timeStamp` | Unix time, present when webhook security (a secret) is on |
| `platform` | Social network the event came from |
| `id` | Post, message or comment id (not unique alone: combine with `subAction`) |

Messages events add `message`, `senderId`, `senderDetails.name`, `conversationId`, `recipientId` and `attachments`. Scheduled events add `status`, `errors` and `postIds`, which is where you find out that a post failed on one network but succeeded on another.

This is the `ads` sample from the [webhook samples library](https://github.com/hookdeck/webhook-samples/blob/main/providers/ayrshare/latest/ads.json):

```json
{
  "action": "ads",
  "type": "accountRevoked",
  "platform": "google",
  "accountId": null,
  "id": "c4e6a8b0d2f4e6a8c0b2d4f6e8a0c2b4d6f8e0a2c4b6d8f0a2c4e6b8d0f2a4c6",
  "created": "2026-10-21T08:15:00.000Z",
  "hookId": "c4e6a8b0d2f4e6a8c0b2d4f6e8a0c2b4d6f8e0a2c4b6d8f0a2c4e6b8d0f2a4c7",
  "refId": "9abf1426d6ce9122ef11c72bd62e59807c5cc083",
  "url": "https://api.myapp.com/Webhook/Ayrshare/Ads"
}

```

## Setting up Ayrshare webhooks

### Choose and set the secret

There is **one secret per User Profile**, shared by all of that profile's webhook actions, so setting it for one action changes it for all of them. Multi-profile accounts manage a separate secret per profile, targeted with the `Profile-Key` header. Generate a long random value yourself, for example with `openssl rand -hex 32`, and set it in one of three ways:

1. Pass `secret` when you register a webhook.
2. Set or rotate it on the **Webhooks** page of the Developer Dashboard.
3. Call `POST /hook/webhook/secret`:

```bash
curl --request POST https://api.ayrshare.com/api/hook/webhook/secret \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --header 'Profile-Key: YOUR_PROFILE_KEY' \
  --data '{ "secret": "your-new-signing-secret" }'

```

The secret is used as typed, as UTF-8 bytes. It is not hex- or base64-decoded.

### Register each action

Register one webhook per action you want to receive:

```bash
curl --request POST https://api.ayrshare.com/api/hook/webhook \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "action": "scheduled",
    "url": "https://your-domain.com/webhooks/ayrshare",
    "secret": "your-signing-secret"
  }'

```

Register on the Primary Profile only if every user's events can go to the same URL. Register on individual User Profiles when a profile needs its own URL.

With Hookdeck, create a source with the Ayrshare source type, enter the profile's secret as its verification secret, and register the Hookdeck source URL with Ayrshare. If you have not registered a secret, leave verification off on the source, because those deliveries carry no signature. For local work, `npx hookdeck-cli listen 3000 ayrshare --path /webhooks/ayrshare` forwards events to your machine through the [Hookdeck CLI](/docs/cli).

### Rotate without dropping deliveries

When you rotate the secret, Ayrshare signs deliveries with both the new and the previous secret for 24 hours, and the V2 header carries both `v1=` values. Update your receiver within that window. The legacy header only ever carries the current signature, which is why the V2 header is the one to verify. See [verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ayrshare-webhooks/references/verification.md) in the Ayrshare skill for worked examples.

## Securing Ayrshare webhooks

Split `x-authorization-content-sha256-v2` on commas, keep the values that start with `v1=`, and accept the request if any of them matches a hex HMAC-SHA256 of the raw body keyed with the profile's secret. Check each candidate is 64 hex characters before decoding it. Ayrshare's [rotation documentation](https://www.ayrshare.com/docs/apis/webhooks/rotate-signing-secret) describes the same check.

```javascript
const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-authorization-content-sha256-v2'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = signature.split(',').map((part) => part.trim())
    .filter((part) => part.startsWith('v1=')).map((part) => part.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

```

A receiver that routes on `action` and records `hookId` before acting:

```javascript
const express = require('express');

const app = express();

app.post('/webhooks/ayrshare', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.AYRSHARE_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

  let hook;
  try {
    hook = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!hook || typeof hook !== 'object' || Array.isArray(hook)) return res.sendStatus(400);
  try {
    if (await alreadyHandled(hook.hookId)) return res.sendStatus(200);

    if (hook.action === 'scheduled') {
      await updatePostStatus(hook.refId, hook.id, hook.status, hook.errors);
    } else if (hook.action === 'messages' || hook.action === 'comments') {
      await addToInbox(hook.refId, hook);
    } else if (hook.action === 'social' && hook.type === 'unlink') {
      await flagDisconnectedAccount(hook.refId, hook.platform);
    }
  } catch {
    return res.sendStatus(503);
  }

  res.sendStatus(200);
});

```

`alreadyHandled`, `updatePostStatus`, `addToInbox` and `flagDisconnectedAccount` stand for your own storage and business logic.

`X-Authorization-Timestamp` is not part of the signed content, so it gives no protection against a replayed request. Ayrshare's overview mentions replay attacks, but only `hookId` deduplication actually stops a repeat from being processed twice. Hookdeck's Ayrshare source verifies the V2 header, tries every `v1=` value, and applies no timestamp check. It answers Ayrshare with `200` and records a failed verification afterwards, rather than returning the `401` shown above.

> **Make Ayrshare webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies Ayrshare's V2 signature through secret rotations, stores every post, message and comment event, and delivers it to your service with retries you control.

## Ayrshare webhook limitations and pain points

### A `4xx` is final

**The Problem:** A short-lived error on your side loses the event for good.

**Why It Happens:** Ayrshare retries `408`, `425`, `429`, `5xx`, timeouts and dropped connections. Any other `4xx` is treated as final. A receiver that returns `401` because it still has the old secret after the 24-hour rotation window, or `404` during a bad deploy, gets no second chance.

**Workarounds:**

* Return `5xx` for errors that might clear up, such as a database outage, and reserve `4xx` for requests you never want again.
* Update every receiver's secret well inside the 24-hour rotation window.

**How Hookdeck Can Help:** Hookdeck answers Ayrshare itself, so Ayrshare's retry rules apply only to Hookdeck. Your service's errors, including `4xx`, are retried on Hookdeck's schedule and can be replayed later.

### Signing is optional and easy to miss

**The Problem:** Requests arrive with no signature header, and anyone who learns the URL can send fake events.

**Why It Happens:** Ayrshare signs only when the profile has a secret. Each User Profile has its own secret, so a new profile may start unsigned even if the Primary Profile is signed.

**Workarounds:**

* Set a secret on every profile as part of onboarding a user.
* Reject unsigned requests in production rather than skipping verification.

**How Hookdeck Can Help:** Hookdeck's Ayrshare source records a failed verification for any unsigned delivery once verification is on, so missing secrets show up in the request log instead of passing silently.

### Retries carry different bytes

**The Problem:** Deduplicating on a body hash or the signature lets retries through.

**Why It Happens:** Each attempt rebuilds the payload, so `timeStamp` and the signature change between attempts. `X-Ayrshare-Delivery-Id` also changes, because it identifies the attempt rather than the event.

**Workarounds:**

* Deduplicate on `hookId`, which Ayrshare keeps the same across retries.
* Keep processed `hookId` values for at least 24 hours, since retries can arrive up to about 16 hours after the event.

**How Hookdeck Can Help:** Hookdeck's deduplication can key on the body's `hookId`, so retried attempts are dropped before they reach your service.

### Sub-actions aren't fully listed

**The Problem:** A new `subAction` or `type` value arrives that your code has never seen.

**Why It Happens:** Ayrshare documents `subAction` values in examples and descriptions, not as a complete list per action, and some examples omit the field.

**Workarounds:**

* Treat `subAction` and `type` as optional, and acknowledge values you don't recognise with `200`.
* Log unknown values so you can add handling later.

**How Hookdeck Can Help:** Hookdeck filters can route on `action` and `subAction`, and the request log shows every value Ayrshare has sent.

## Best practices

### Verify the V2 header, not the legacy one

The V2 header keeps working through a secret rotation because it can carry both signatures. The bare `X-Authorization-Content-SHA256` header only holds the current one.

### Use the raw request body

Verify before parsing. Use `express.raw({ type: '*/*' })` in Express, `await request.arrayBuffer()` in Next.js and `await request.body()` in FastAPI, and never re-serialise the JSON before checking it.

### Answer within 15 seconds

Store the verified event, return `200`, and then update posts or inboxes. A slow answer counts as a failure and triggers a retry. Read more in [why you should process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Route by profile with `refId`

When many users share the Primary Profile's webhook, `refId` tells you which User Profile an event belongs to. Look up the user by `refId` before you touch any data.

### Deduplicate on `hookId`

Store each `hookId` with the change it caused. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Ayrshare webhooks are registered per action and per profile, signed only once a profile has a secret, and verified through the V2 header that survives a 24-hour rotation. Give every profile a secret, return `5xx` rather than `4xx` for errors that might pass, deduplicate on `hookId`, and use `refId` to find the user each event belongs to.

[Hookdeck Event Gateway](https://hookdeck.com) takes Ayrshare's deliveries, checks the V2 signature, and retries your service on its own schedule, so a `4xx` on your side no longer ends the event's life.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Ayrshare webhooks reliably in minutes.