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

# API and webhooks

> Create API keys for the Triggr Public API and send signed events to your portal or PSA integration.

Only admins can manage API keys and webhooks. Endpoints, scopes and a playground are in the [API reference](/api-reference/introduction).

## Create an API key

<Steps>
  <Step title="Open Settings > API">
    Go to **Settings > API** and click **Create key**.

    <Frame>
      <img src="https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/api-keys.png?fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=2a8005602ae281f4acf35400a1f72f57" alt="The API keys tab with a Create key button and a table listing Client portal with core:run and executions:manage scopes and PSA sync with read + embed, both Active, each with Roll and Revoke actions." width="2080" height="819" data-path="images/core/api-and-webhooks/api-keys.png" />
    </Frame>
  </Step>

  <Step title="Name it and tick its scopes">
    Every key can read your clients, forms, workflows, executions and Core requests, and embed forms as a signed-in user. Tick a scope only if the integration needs it, then click Create key.

    <Frame>
      <img src="https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/api-create.png?fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=752284bcd2ad1a4ff8b11f9c72e22161" alt="The Create API key dialog with Name Client portal and three scope checkboxes: Run Core forms natively core:run, ticked, Configure Core workflows core:configure, and Act on running work executions:manage." width="1024" height="1190" data-path="images/core/api-and-webhooks/api-create.png" />
    </Frame>
  </Step>

  <Step title="Copy the key">
    Click **Copy** before Done: the key is shown once. Keys expire after a year.

    <Frame>
      <img src="https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/api-created.png?fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=4859d715f96542a8f745499dba92205b" alt="The Copy your API key dialog showing a key beginning tpk_ with a Copy button, the note Send it as Authorization: Bearer <key>, and a Done button." data-og-width="1024" width="1024" data-og-height="550" height="550" data-path="images/core/api-and-webhooks/api-created.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/api-created.png?w=280&fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=38b39446f304a804b5830f98676e63ee 280w, https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/api-created.png?w=560&fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=adb468473a73edb1521f615a8d9fc7af 560w, https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/api-created.png?w=840&fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=e1fd8a4409c3ac0b1fda878411128536 840w, https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/api-created.png?w=1100&fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=79e6bcd0c6cd47ea6854ad81ee9a3c92 1100w, https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/api-created.png?w=1650&fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=8108cf0b453c12e8da0c1b3bfd07da24 1650w, https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/api-created.png?w=2500&fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=d67e6866166b31409436dd23dfbc3b56 2500w" />
    </Frame>
  </Step>
</Steps>

| Scope | Lets the key |
| - | - |
| `core:run` | Submit Core forms from your own UI, through the same approval as Core. |
| `core:configure` | Read and change Core configuration, including each client's groups, licences and mailboxes. |
| `executions:manage` | Decide approvals, and cancel or retry executions. |

Roll issues a replacement and keeps the old key working for 1, 24 or 72 hours (or until it expires, if sooner). Revoke stops it at once and disables its webhooks.

## Add a webhook

<Steps>
  <Step title="Open Settings > Webhooks">
    Go to **Settings > Webhooks** and click **Add webhook**.

    <Frame>
      <img src="https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/webhooks.png?fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=b1b6df0fb2cf27fbb24bfaad7e769a8e" alt="The Webhooks page with Refresh and Add webhook buttons and a table listing https://portal.yourmsp.com/hooks/triggr, Portal notifications, subscribed to execution.completed, execution.failed, approval.requested and approval.decided, Active, last delivery 13 minutes ago, registered by Core, with Test, Deliveries, Rotate secret and Delete actions." width="2080" height="839" data-path="images/core/api-and-webhooks/webhooks.png" />
    </Frame>
  </Step>

  <Step title="Enter the endpoint and events">
    The Endpoint URL must be public `https`; redirects aren't followed. Tick the events you want, or All events to include future ones, then click Add webhook.

    <Frame>
      <img src="https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/webhook-add.png?fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=a12f399f49428165cd47ca40ef1de7aa" alt="The Add webhook dialog with Endpoint URL https://psa.yourmsp.com/webhooks/triggr, Description PSA integration, All events ticked, and Cancel and Add webhook buttons." width="1024" height="774" data-path="images/core/api-and-webhooks/webhook-add.png" />
    </Frame>
  </Step>

  <Step title="Copy the signing secret">
    Click **Copy** before Done: the secret is shown once.

    <Frame>
      <img src="https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/webhook-added.png?fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=03fae438fbd20286553a20b2d2477dc4" alt="The Webhook added dialog showing a signing secret beginning whsec_ with a Copy button, a note on the X-Triggr-Signature header, and a Done button." width="1152" height="630" data-path="images/core/api-and-webhooks/webhook-added.png" />
    </Frame>
  </Step>

  <Step title="Test it">
    Click **Test** on the row to send a `ping`, then Deliveries to see each attempt's HTTP code and error.

    <Frame>
      <img src="https://mintcdn.com/triggr/MwzXHG3UWzFixdSz/images/core/api-and-webhooks/webhook-deliveries.png?fit=max&auto=format&n=MwzXHG3UWzFixdSz&q=85&s=d2c380942b80b726083abf7bbe88ac0b" alt="The Recent deliveries dialog for https://portal.yourmsp.com/hooks/triggr listing execution.completed delivered with HTTP 200, approval.requested pending after 2 attempts with HTTP 503 and a next attempt time, and a ping delivered with HTTP 200." width="1792" height="768" data-path="images/core/api-and-webhooks/webhook-deliveries.png" />
    </Frame>
  </Step>
</Steps>

| Event | Sent when |
| - | - |
| `core.request.submitted` | A Core form is submitted. |
| `execution.completed`, `execution.failed` | A run finishes or fails. |
| `approval.requested`, `approval.decided`, `approval.expired` | An approval is created, decided or expires. |
| `client.created`, `client.updated`, `client.deleted` | A client changes. |
| `core_form.updated` | A Core form's settings change. |
| `form.created`, `form.updated`, `form.deleted` | A Triggr Studio form changes. |
| `ping` | You click Test. |

Triggr POSTs JSON, normally within a minute, with `X-Triggr-Event`, `X-Triggr-Delivery-Id` and `X-Triggr-Signature` headers. `data` is the object the event is about; `id` stays the same across retries, so deduplicate on it.

```json theme={null}
{
  "id": "3f2b9c1e-…",
  "event": "ping",
  "createdAt": "2026-10-12T09:00:00Z",
  "organisationId": "org_…",
  "data": { "message": "Triggr webhook test", "at": "2026-10-12T09:00:00Z" }
}
```

### Verify the signature

`X-Triggr-Signature` is `t=<unix>,v1=<hex>`: an HMAC-SHA256 of `t + "." + rawBody` with your secret. Reject a `t` older than five minutes. For 24 hours after you click Rotate secret, the header carries two `v1` values; accept either.

```js theme={null}
const crypto = require('node:crypto')

// rawBody is the unparsed request body (a Buffer or string)
function verifyTriggrWebhook(header, rawBody, secret) {
  const fields = (header || '').split(',')
  const t = fields.find((f) => f.startsWith('t='))?.slice(2)
  const sigs = fields.filter((f) => f.startsWith('v1=')).map((f) => f.slice(3))
  if (!/^\d+$/.test(t || '') || !sigs.length) return false
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  return sigs.some((v1) => /^[0-9a-f]{64}$/.test(v1) && crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex')))
}
```

### Retries

Answer 2xx within 15 seconds. Triggr retries after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. After 25 failures in a row the webhook shows Disabled: fix the endpoint and click Enable. Deliveries keeps 30 days; Redeliver sends one again, and Delete stops deliveries and removes the history.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Settings > API or Webhooks says only organisation admins can manage them">
    Ask an [Admin](/core/get-started/invite-your-team#invite-teammates) to create the key or webhook, or to change your role.
  </Accordion>

  <Accordion title="Test fails straight away">
    The URL must be `https`, on a public host, and answer within 15 seconds without redirecting. Private addresses are refused.
  </Accordion>

  <Accordion title="Signatures don't verify">
    Compute the HMAC over the raw request body, not a re-serialised copy, and check your endpoint's clock: the sample code rejects a `t` more than five minutes out.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.