> ## 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.

# Rotate the signing secret

> Issues a new secret (returned ONCE) and keeps the previous one for 24 hours: until `previousSecretValidUntil` every delivery's `X-Triggr-Signature` carries two `v1` values — new secret first, previous second — so switch your receiver whenever convenient inside that window.

Rotating again inside that window KEEPS the older secret — the one your receiver is actually verifying with — rather than promoting the current one, so a retried rotation can never leave your endpoint unable to verify anything. The window still runs from the FIRST rotation; it is not extended. Send an `Idempotency-Key` and a retry replays instead of minting another secret.



## OpenAPI

````yaml /api-reference/openapi.json post /api/v1/webhooks/{webhookId}/rotate-secret
openapi: 3.0.3
info:
  description: >-
    The API an MSP's own portal (or a partner acting for it) uses to wrap
    Triggr: list clients, forms, workflows, executions and Core requests; embed
    forms as a signed-in user; run Core forms natively; act on approvals and
    manual steps; configure Core workflows; and receive events by webhook.


    Authentication: every request carries an organisation API key as
    `Authorization: Bearer tpk_…` (issued on the API Keys settings page; shown
    once). Every key can read the catalogue and mint form sessions; `core:run`,
    `core:configure` and `executions:manage` are opt-in scopes chosen when the
    key is created — see `x-triggr-scopes` and each operation's
    `x-triggr-scope`.


    Rate limits: 600 requests/min per key (bursts to 200) for reads and 60/min
    (bursts to 30) for writes; form-session mints have their own 120/min (bursts
    to 120). Authenticated responses carry `X-RateLimit-Limit` /
    `X-RateLimit-Remaining` (exposed to browsers); a 429 carries `Retry-After`.


    Paging: lists take `limit` (default 50, max 100) and `offset` and return
    `total` + `hasMore`; `since` (RFC 3339) is accepted where an operation
    declares it (executions, Core requests).


    Idempotency is OPTIONAL and opt-in: send nothing and every endpoint behaves
    exactly as before — existing integrations need change nothing. The
    operations that start or decide something (run a Core form, approve /
    reject, complete a manual step, cancel / retry an execution, register a
    webhook) additionally accept an `Idempotency-Key` header (1–255 characters;
    use a UUID per request). Retry a timed-out call with the same key and the
    same method, path and body within 24 hours and you get the first response
    back — status and body — flagged `Idempotent-Replayed: true`, instead of a
    second run. The same key with a different request is refused with 422; a
    repeat while the first is still in flight is a 409; a 5xx is never stored,
    so that retry runs for real. Keys are scoped to the API key that sent them.


    One-time values are the exception, and deliberately so: a webhook signing
    `secret` and a Core run's `secret` (the new starter's temporary password)
    are never written to our store, so a REPLAYED response carries `"secret":
    null` and an `idempotentReplayNote` saying where to get a fresh one. Record
    those from the original response — a replay cannot give them back.


    Webhooks: a delivery is a POST carrying `X-Triggr-Signature`,
    `X-Triggr-Event`, `X-Triggr-Delivery-Id` and `User-Agent:
    Triggr-Webhooks/1.0`, with a `WebhookEnvelope` body. Mind the two ids: the
    envelope's `id` names the EVENT and is unchanged across retries and
    redeliveries, so it is your dedupe key; `X-Triggr-Delivery-Id` names the
    DELIVERY ROW — the same on every retry of that delivery, new on a redelivery
    — and matches a row in the Deliveries list. Verify the signature by
    accepting the header if any `v1` matches your secret — there are two for 24
    hours after a rotation.


    ## Embedding


    **Which forms can be embedded.** A form can be embedded in your portal
    exactly when its `embeddable` field is true (every form in `GET
    /api/v1/forms`, `GET /api/v1/forms/{formId}` and the Core forms list and
    detail carries it). By auth type: `microsoft` and `google` — yes, as the
    signed-in user, through a form session; `none` (public) — yes, through a
    form session that attributes the submission to your portal user and client,
    or plainly from `shareUrl` (anonymous); `password` and `internal` — no: a
    session request is refused with `form_session_unsupported`, and neither can
    sign a visitor in inside a third-party frame. Core forms are embeddable when
    `public` (Microsoft sign-in). Branch on `embeddable`, not on your own list
    of auth types.


    **When a session ends.** Inside your frame, the form page tells your page
    when its embedded session can't be used, with `window.parent.postMessage({
    type: "triggr:form-session", state }, "*")` — exactly those two fields, no
    token. `state` is `"error"` when the handle couldn't be redeemed (already
    used, expired, or rate-limited past its retry) and `"expired"` when the
    signed-in session lapsed (noticed on reload or submit — and, on a Core form,
    on a lookup) — at most once per lapse. Nothing is posted when the page isn't
    framed. Listen for it, check `event.origin` equals your stored embed origin
    (`formsOrigin` / `coreFormsOrigin`), then mint a fresh session and replace
    the iframe's `src` with its `embedUrl`; without that, the visitor is left on
    a "reopen the form" message.


    **The embed host.** Every `embedUrl` and `shareUrl` begins with the
    organisation's canonical embed origin: `scheme://host[:port]` — `https`,
    lower-case host, no path, no trailing slash, no default port. It is the same
    for every form and session and does not change between releases — only if
    you change your forms domain — and `GET /api/v1/me` returns it as
    `formsOrigin` (Studio) and `coreFormsOrigin` (Core). Store that value as
    your embed host, and before framing a session compare it byte-for-byte with
    the ORIGIN of the returned `embedUrl` (`new URL(embedUrl).origin`; in .NET
    `new Uri(embedUrl).GetLeftPart(UriPartial.Authority)`). Worked example —
    stored embed host `https://forms.acme-msp.com`; `embedUrl`
    `https://forms.acme-msp.com/forms/org_2Ab3/9c8d…?formSession=3f9a…` → origin
    `https://forms.acme-msp.com` → match. With no custom forms domain the origin
    is `https://app.triggr-app.com` (Studio) or `https://core.triggr-app.com`
    (Core). If you store a bare host instead (`forms.acme-msp.com`), compare it
    with the URL's host component, lower-cased.


    Errors are `{ "error": "…" }`: 400 your request, 401 the key, 403 scope or
    not your client, 404 not found, 412 a stale If-Match, 429 rate limit, 503 a
    dependency such as the on-prem agent. The two form-session endpoints instead
    answer `{ "error": { "code", "message", "retryable" } }` (SessionError).
    Timestamps are RFC 3339 UTC.
  title: Triggr Public API
  version: 1.4.0
servers:
  - url: https://api.triggr-app.com
security:
  - ApiKeyAuth: []
tags:
  - description: Who this key is, and the clients every other endpoint scopes by.
    name: Organisation
  - description: >-
      Studio forms with shareable embed URLs, and form sessions that embed a
      `microsoft` or `google` form as an already-signed-in user. Branch on each
      form's `embeddable` (see Embedding): `microsoft` and `google` forms embed
      as the signed-in user, `none` forms embed with a session attributing the
      submission to your portal user (or plainly from `shareUrl`), and
      `password` / `internal` forms cannot be embedded.
    name: Studio forms
  - description: >-
      The Studio workflow catalogue and a status feed of runs: what ran, for
      which client, how it ended. Outcome only — never parameters, step context
      or logs.
    name: Workflows & executions
  - description: >-
      Core forms (onboarding, offboarding, …) with their configuration status,
      embedded sessions for the public ones, and every request submitted through
      them with live status.
    name: Core forms
  - description: >-
      Render and submit a Core form in your own UI as a technician. Scope
      `core:run`.
    name: Core run
  - description: >-
      Read and write Core configuration. A form has one base config and optional
      per-client overrides; writes merge (omitted keeps, formConfig/execConfig
      replace as units, null clears, `inherit` clears a client's availability);
      read the `etag` and send If-Match to write atomically. Scope
      `core:configure`.
    name: Core configuration
  - description: >-
      The work waiting for a person. Read with any key; decide with scope
      `executions:manage`, naming the actor.
    name: Approvals & manual steps
  - description: >-
      Signed event delivery to your endpoint, so you don't poll. Register a URL,
      verify `X-Triggr-Signature` (accept the header if any `v1` matches — there
      are two for 24 hours after a secret rotation), inspect and re-send
      deliveries.
    name: Webhooks
  - description: This document.
    name: Meta
paths:
  /api/v1/webhooks/{webhookId}/rotate-secret:
    post:
      tags:
        - Webhooks
      summary: Rotate the signing secret
      description: >-
        Issues a new secret (returned ONCE) and keeps the previous one for 24
        hours: until `previousSecretValidUntil` every delivery's
        `X-Triggr-Signature` carries two `v1` values — new secret first,
        previous second — so switch your receiver whenever convenient inside
        that window.


        Rotating again inside that window KEEPS the older secret — the one your
        receiver is actually verifying with — rather than promoting the current
        one, so a retried rotation can never leave your endpoint unable to
        verify anything. The window still runs from the FIRST rotation; it is
        not extended. Send an `Idempotency-Key` and a retry replays instead of
        minting another secret.
      parameters:
        - in: path
          name: webhookId
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          content:
            application/json:
              example:
                previousSecretValidUntil: '2026-09-08T14:12:00Z'
                secret: whsec_7Qm4Xt9bZa2vLp6Rk0Yc3Nd8Fe1Ws5Hj
                webhook:
                  active: true
                  consecutiveFailures: 0
                  createdAt: '2026-09-01T08:30:00Z'
                  description: Acme portal
                  events:
                    - execution.completed
                    - execution.failed
                    - approval.requested
                  id: e2b7c4d9-1f8a-4c36-b5e0-7a9d2f4c8b13
                  lastDeliveryAt: '2026-09-07T10:02:20Z'
                  url: https://portal.example.com/hooks/triggr
              schema:
                properties:
                  idempotentReplayNote:
                    description: >-
                      Present only on an Idempotency-Key replay whose original
                      response carried a one-time credential: says that the
                      value is shown only in the original response and how to
                      issue a fresh one
                    type: string
                  previousSecretValidUntil:
                    format: date-time
                    type: string
                  secret:
                    description: >-
                      The new signing secret, shown ONCE. null on an
                      Idempotency-Key replay — see idempotentReplayNote
                    nullable: true
                    type: string
                  webhook:
                    $ref: '#/components/schemas/Webhook'
                required:
                  - webhook
                  - secret
                  - previousSecretValidUntil
                type: object
          description: OK
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Missing, invalid, revoked, or expired API key
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Not found (or belongs to another organisation)
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: >-
            A request with this Idempotency-Key is still in progress — retry
            shortly
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Idempotency-Key was already used with a different request
        '429':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Rate limit exceeded for this key — see Retry-After
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - label: curl
          lang: curl
          source: >-
            # Idempotency-Key is optional: send one to make a retry safe.

            curl -X POST
            "https://api.triggr-app.com/api/v1/webhooks/<webhookId>/rotate-secret"
            \
              -H "Authorization: Bearer $TRIGGR_API_KEY" \
              -H "Idempotency-Key: $(uuidgen)"
        - label: Node
          lang: node
          source: >-
            // Idempotency-Key is optional: send one to make a retry safe.

            const res = await
            fetch("https://api.triggr-app.com/api/v1/webhooks/<webhookId>/rotate-secret",
            {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.TRIGGR_API_KEY}`,
                "Idempotency-Key": crypto.randomUUID(),
              },
            });

            if (!res.ok) throw new Error(`Triggr ${res.status}: ${await
            res.text()}`);

            const data = await res.json();
        - label: PowerShell
          lang: powershell
          source: >-
            # Idempotency-Key is optional: send one to make a retry safe.

            $headers = @{
              Authorization = "Bearer $env:TRIGGR_API_KEY"
              'Idempotency-Key' = [guid]::NewGuid().ToString()
            }

            Invoke-RestMethod -Method Post -Uri
            'https://api.triggr-app.com/api/v1/webhooks/<webhookId>/rotate-secret'
            -Headers $headers
components:
  parameters:
    IdempotencyKey:
      description: >-
        OPTIONAL — omit it and this operation behaves exactly as it always has.
        A unique value (a UUID) you choose per request. A retry with the same
        key and the same method, path and body within 24 hours gets the first
        response replayed (status and body, with `Idempotent-Replayed: true`)
        instead of running again; the same key with a different request is
        refused with 422. A 5xx is not stored, so that retry runs. A one-time
        credential is never stored: on a replay any `secret` is null, with an
        `idempotentReplayNote` explaining it.
      in: header
      name: Idempotency-Key
      required: false
      schema:
        maxLength: 255
        minLength: 1
        type: string
  schemas:
    Webhook:
      properties:
        active:
          type: boolean
        consecutiveFailures:
          type: integer
        createdAt:
          format: date-time
          type: string
        description:
          type: string
        disabledReason:
          type: string
        events:
          items:
            type: string
          type: array
        id:
          type: string
        lastDeliveryAt:
          format: date-time
          type: string
        lastError:
          type: string
        url:
          format: uri
          type: string
      required:
        - id
        - url
        - events
        - active
        - consecutiveFailures
        - createdAt
      type: object
    Error:
      properties:
        error:
          type: string
      required:
        - error
      type: object
  securitySchemes:
    ApiKeyAuth:
      bearerFormat: tpk_…
      description: >-
        Organisation API key. Scoped operations carry x-triggr-scope; the scopes
        themselves are described in x-triggr-scopes.
      scheme: bearer
      type: http

````

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