> ## Documentation Index
> Fetch the complete documentation index at: https://www.ayrshare.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Rotate Signing Secret

> Safely rotate your webhook signing secret with a 24-hour dual-signing grace window

export const PlansAvailable = ({plans = [], maxPackRequired}) => {
  let displayPlans = plans;
  if (plans && plans.length === 1) {
    const lowerCasePlan = plans[0].toLowerCase();
    if (lowerCasePlan === "business") {
      displayPlans = ["Launch", "Business", "Enterprise"];
    } else if (lowerCasePlan === "premium") {
      displayPlans = ["Premium", "Launch", "Business", "Enterprise"];
    }
  }
  return <Note>
Available on {displayPlans.length === 1 ? "the " : ""}
{displayPlans.join(", ").replace(/\b\w/g, l => l.toUpperCase())}{" "}
{displayPlans.length > 1 ? "plans" : "plan"}.

{maxPackRequired && <span onClick={() => window.open('https://www.ayrshare.com/docs/additional/maxpack', '_self')} className="flex items-center mt-2 cursor-pointer">
 <span className="px-1.5 py-0.5 rounded text-sm" style={{
    backgroundColor: '#C264B6',
    color: 'white',
    fontSize: '12px'
  }}>
   Max Pack required
 </span>
</span>}
</Note>;
};

export const HeaderAPI = ({noProfileKey, profileKeyRequired}) => <>
    <ParamField header="Authorization" type="string" required>
      <a href="/docs/apis/overview#authorization">API Key</a> of the Primary Profile.
      <br />
      <br />
      Format: <code>Authorization: Bearer API_KEY</code>
    </ParamField>
    {!noProfileKey && (profileKeyRequired ? <ParamField header="Profile-Key" type="string" required>
          <a href="/docs/apis/overview#profile-key-format">Profile Key</a> of a User Profile.
          <br />
          <br />
          Format: <code>Profile-Key: PROFILE_KEY</code>
        </ParamField> : <ParamField header="Profile-Key" type="string">
          <a href="/docs/apis/overview#profile-key-format">Profile Key</a> of a User Profile.
          <br />
          <br />
          Format: <code>Profile-Key: PROFILE_KEY</code>
        </ParamField>)}
  </>;

<PlansAvailable plans={["premium"]} maxPackRequired={false} />

## Overview

Ayrshare signs every webhook delivery with an [HMAC-SHA256](https://en.wikipedia.org/wiki/HMAC) of the payload, keyed by your **signing secret**, so your receiver can confirm a delivery genuinely came from Ayrshare. See [Webhook Security](/docs/apis/webhooks/overview#webhook-security) for how verification works.

Rotating your signing secret lets you replace it on a regular schedule, or immediately if you suspect it has been exposed. To make rotation safe, Ayrshare opens a **24-hour grace window** after every rotation during which deliveries are signed with **both** your previous and your new secret. This lets you update your receiver on your own schedule without dropping or rejecting a single delivery — the same pattern used by Stripe and GitHub.

<Note>
  The signing secret is **profile-wide**: there is one secret per User Profile (UID),
  and it signs **every** webhook action that profile has registered. There is no
  per-action signing secret — setting or rotating the secret changes it for all
  actions on that profile at once.
</Note>

## Rotate from the Dashboard

You can set or rotate your signing secret from the [Webhooks page](https://app.ayrshare.com/webhooks) in the Developer Dashboard. The **Signing Secret** panel appears above your webhook list once the profile has at least one registered webhook.

<Steps>
  <Step title="Open the Signing Secret panel">
    Go to the [Webhooks page](https://app.ayrshare.com/webhooks). If no secret is configured yet, the panel shows **No signing secret configured** with a **Set Signing Secret** button. If one is already configured, it shows **Signing secret configured** with a **Rotate** button.
  </Step>

  <Step title="Set or Rotate">
    Click **Set Signing Secret** (first-time) or **Rotate** (existing secret). A modal opens with a strong, randomly generated secret pre-filled and revealed. You can **Copy** it, **Regenerate** a new one, or toggle **paste my own** to supply your own value.
  </Step>

  <Step title="Confirm">
    Copy the secret somewhere safe — it is shown only once and can never be retrieved from the UI again — then confirm to submit. A success toast appears and the panel updates.
  </Step>

  <Step title="Update your receiver">
    On a rotation (not a first-time set), the panel shows an active grace-window indicator and the **Rotate** button is disabled until the window closes. You have 24 hours to deploy the new secret to your receiver.
  </Step>
</Steps>

## Rotate via the API

Rotate (or set) the signing secret with a single call. This creates a new secret, repoints the profile's secret reference to it, and — when an existing secret was in place — records the superseded secret as the previous secret with an expiry 24 hours out.

### Header Parameters

<HeaderAPI />

### Body Parameters

<ParamField body="secret" type="string" required>
  The new signing secret value. Any non-empty string is accepted. We recommend a long, high-entropy random value (for example, 32 random bytes encoded as base64url).
</ParamField>

<RequestExample>
  ```bash cURL theme={"system"}
  curl --request POST \
    --url 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"
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 200: Response theme={"system"}
  {
    "status": "success",
    "action": "webhook",
    "refId": "3dc079614bdc3f281d9" // User Profile Ref Id
  }
  ```
</ResponseExample>

The plaintext `secret` is **never** returned in the response and is never logged. The response carries the client-facing `refId` (a hash of the UID), never the UID itself. The `Profile-Key` header is optional and scopes the rotation to a single User Profile for multi-profile accounts.

A missing or empty `secret` returns a mapped error (`code: 101`, "Missing/incorrect parameter") with an HTTP `400` status, and no change is made to your current secret. First-time set via the API (no existing secret) creates the secret with no previous secret recorded and no grace window.

## Safe Rotation Procedure

Because of the 24-hour grace window, there is no required order of operations — your receiver keeps working throughout. The recommended sequence is:

<Steps>
  <Step title="Rotate the secret">
    Rotate from the dashboard or via the API. Ayrshare immediately begins signing deliveries with both your previous and your new secret.
  </Step>

  <Step title="Update your receiver">
    Within 24 hours, deploy the new secret to your webhook receiver so it verifies against the new value.
  </Step>

  <Step title="Let the window close">
    After 24 hours, Ayrshare automatically clears the previous secret and signs only with the new secret. No further action is needed on your side.
  </Step>
</Steps>

<Tip>
  If you rotate again while a grace window is still open, the just-superseded
  secret becomes the new previous secret and a fresh 24-hour window starts. Only
  one previous secret is kept at a time.
</Tip>

## Verifying Signatures During the Grace Window

Outside of a grace window, signed deliveries carry the standard headers (see [Webhook Security](/docs/apis/webhooks/overview#webhook-security)):

```bash theme={"system"}
X-Authorization-Timestamp : <Unix Timestamp In Seconds>
X-Authorization-Content-SHA256 : <current-sig>
X-Authorization-Content-SHA256-V2 : v1=<current-sig>
```

During the 24-hour window after a rotation, the new `X-Authorization-Content-SHA256-V2` header lists **both** signatures, current first, comma-separated:

```bash theme={"system"}
X-Authorization-Timestamp : <Unix Timestamp In Seconds>
X-Authorization-Content-SHA256 : <current-sig>
X-Authorization-Content-SHA256-V2 : v1=<current-sig>,v1=<previous-sig>
```

<Note>
  `X-Authorization-Content-SHA256` is unchanged: it always carries the single
  current-secret HMAC, for backward compatibility. Dual signatures appear only in
  the new `X-Authorization-Content-SHA256-V2` header.
</Note>

Each value in `X-Authorization-Content-SHA256-V2` is prefixed with a scheme tag. `v1=` denotes an HMAC-SHA256 signature, computed exactly like `X-Authorization-Content-SHA256`. The `-V2` header is always present whenever a delivery is signed — it carries at least `v1=<current-sig>` — so you can rely on it as a stable receiver contract.

To verify a delivery during (or outside) a rotation:

<Steps>
  <Step title="Compute the HMAC">
    Compute the HMAC-SHA256 of the **raw request body** using your locally configured signing secret.
  </Step>

  <Step title="Compare against every listed signature">
    Read `X-Authorization-Content-SHA256-V2`, split it on commas, strip the `v1=` prefix from each value, and accept the delivery as authentic if your computed HMAC matches **any** listed `v1=` signature.
  </Step>
</Steps>

Accepting if **any** listed signature matches is what makes rotation zero-downtime: a receiver still configured with the old secret matches `v1=<previous-sig>`, while a receiver updated to the new secret matches `v1=<current-sig>` — both succeed throughout the window.

### Receiver Verification Example

```javascript Node.js theme={"system"}
import crypto from "crypto";

// secret is the signing secret currently configured on your receiver.
function isAuthenticWebhook(rawBody, headers, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody) // the raw, unparsed request body
    .digest("hex");

  const headerValue = headers["x-authorization-content-sha256-v2"] || "";

  // Accept if ANY v1= signature in the header matches our computed HMAC.
  return headerValue
    .split(",")
    .map((part) => part.trim())
    .filter((part) => part.startsWith("v1="))
    .map((part) => part.slice("v1=".length))
    .some((sig) => {
      const sigBuf = Buffer.from(sig);
      const expectedBuf = Buffer.from(expected);
      // timingSafeEqual throws on length mismatch — treat as not authentic.
      return (
        sigBuf.length === expectedBuf.length &&
        crypto.timingSafeEqual(sigBuf, expectedBuf)
      );
    });
}
```

<Warning>
  Always compute the HMAC over the **raw** request body bytes, exactly as
  received — not over a re-serialized JSON object. Re-serialization can change
  whitespace or key order and break verification. Use a constant-time comparison
  (such as `crypto.timingSafeEqual`) to avoid timing attacks.
</Warning>

If a delivery's current secret record is missing, the delivery proceeds **unsigned** (no signature headers) rather than failing. If only the previous secret record is gone, the previous signature is skipped and the current signature is still emitted in both headers.
