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

# Start an embedded form session

> Mints a handle asserting the given user into a `microsoft` or `google` authType form and returns the `embedUrl` to iframe: the visitor lands already signed in. For a microsoft form give the user's object ID or UPN plus their tenant (`microsoftTenantId`, or `clientId` and it is looked up); for a google form the Workspace email alone. A `none` (public) form takes a session too: give the portal user's identifier and their `clientId` (or `microsoftTenantId`), and the submission is attributed to them and that client (`provider` `portal`) — nothing is looked up in a directory. With no `user`, a public form returns its plain `embedUrl` (`provider` `none`, no handle) and submits anonymously, as before. `password` and `internal` forms cannot take a session. Refusals carry a SessionError code. The handle is single-use and expires after 2 minutes: mint one per page load, server-side. The user must belong to a client this organisation manages — their Microsoft tenant connected, or their Google Workspace domain mapped to a client — otherwise 403. The signed-in session lasts 30 minutes from when you mint it; after that a submit is refused and the page must be reloaded with a fresh handle. Every session is audited against the key that minted it, and a submission — and every workflow it triggers, as `metadata.custom.apiKeyId` — records which key embedded the form. The form's own access rules still apply to the asserted identity.



## OpenAPI

````yaml /api-reference/openapi.json post /api/v1/forms/{formId}/sessions
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/forms/{formId}/sessions:
    post:
      tags:
        - Studio forms
      summary: Start an embedded form session
      description: >-
        Mints a handle asserting the given user into a `microsoft` or `google`
        authType form and returns the `embedUrl` to iframe: the visitor lands
        already signed in. For a microsoft form give the user's object ID or UPN
        plus their tenant (`microsoftTenantId`, or `clientId` and it is looked
        up); for a google form the Workspace email alone. A `none` (public) form
        takes a session too: give the portal user's identifier and their
        `clientId` (or `microsoftTenantId`), and the submission is attributed to
        them and that client (`provider` `portal`) — nothing is looked up in a
        directory. With no `user`, a public form returns its plain `embedUrl`
        (`provider` `none`, no handle) and submits anonymously, as before.
        `password` and `internal` forms cannot take a session. Refusals carry a
        SessionError code. The handle is single-use and expires after 2 minutes:
        mint one per page load, server-side. The user must belong to a client
        this organisation manages — their Microsoft tenant connected, or their
        Google Workspace domain mapped to a client — otherwise 403. The
        signed-in session lasts 30 minutes from when you mint it; after that a
        submit is refused and the page must be reloaded with a fresh handle.
        Every session is audited against the key that minted it, and a
        submission — and every workflow it triggers, as
        `metadata.custom.apiKeyId` — records which key embedded the form. The
        form's own access rules still apply to the asserted identity.
      parameters:
        - in: path
          name: formId
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            example:
              user:
                clientId: c1d2e3f4-5a6b-7c8d-9e0f-1a2b3c4d5e6f
                userIdentifier: jane.doe@contoso.com
            schema:
              $ref: '#/components/schemas/FormSessionRequest'
        required: true
      responses:
        '200':
          content:
            application/json:
              example:
                embedUrl: >-
                  https://app.triggr-app.com/telescope/forms/org_…/9c8d…?formSession=3f9a…e21c
                expiresIn: 120
                formSessionHandle: 3f9a…e21c
                provider: microsoft
              schema:
                $ref: '#/components/schemas/FormSession'
          description: OK
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionError'
          description: >-
            `invalid_request`, `form_session_unsupported`, `form_unpublished`,
            `client_not_entitled` or `invalid_user_identifier` — see
            SessionError
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionError'
          description: '`invalid_api_key`'
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionError'
          description: '`tenant_not_connected` or `microsoft_permission_denied`'
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionError'
          description: >-
            `form_not_found`, `client_not_found`, `client_not_connected` or
            `user_not_found` — the code tells them apart
        '429':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionError'
          description: '`rate_limited` — see `Retry-After`'
        '503':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionError'
          description: '`microsoft_unavailable` — retry after the `Retry-After` seconds'
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - label: curl
          lang: curl
          source: >-
            curl -X POST
            "https://api.triggr-app.com/api/v1/forms/<formId>/sessions" \
              -H "Authorization: Bearer $TRIGGR_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{
              "user": {
                "userIdentifier": "jane.doe@contoso.com",
                "clientId": "c1d2e3f4-5a6b-7c8d-9e0f-1a2b3c4d5e6f"
              }
            }'
        - label: Node
          lang: node
          source: >-
            const res = await
            fetch("https://api.triggr-app.com/api/v1/forms/<formId>/sessions", {
              method: "POST",
              headers: {
                "Authorization": `Bearer ${process.env.TRIGGR_API_KEY}`,
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "user": {
                  "userIdentifier": "jane.doe@contoso.com",
                  "clientId": "c1d2e3f4-5a6b-7c8d-9e0f-1a2b3c4d5e6f"
                }
              }),
            });

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

            const data = await res.json();
        - label: PowerShell
          lang: powershell
          source: >-
            $headers = @{
              Authorization = "Bearer $env:TRIGGR_API_KEY"
            }

            $body = @'

            {
              "user": {
                "userIdentifier": "jane.doe@contoso.com",
                "clientId": "c1d2e3f4-5a6b-7c8d-9e0f-1a2b3c4d5e6f"
              }
            }

            '@

            Invoke-RestMethod -Method Post -Uri
            'https://api.triggr-app.com/api/v1/forms/<formId>/sessions' -Headers
            $headers -ContentType 'application/json' -Body $body
components:
  schemas:
    FormSessionRequest:
      properties:
        user:
          properties:
            clientId:
              description: >-
                Alternative to microsoftTenantId: the client the user belongs to
                (GET /api/v1/clients). For a none form, the client the
                submission is attributed to
              type: string
            microsoftTenantId:
              description: >-
                The user's Microsoft tenant ID. Required for microsoft forms
                unless clientId is given; ignored for google forms
              type: string
            userIdentifier:
              description: >-
                Microsoft object ID or UPN (microsoft), Google Workspace email
                (google), or the portal user's identifier, e.g. their email
                (none — optional)
              type: string
          type: object
      type: object
    FormSession:
      properties:
        embedUrl:
          format: uri
          type: string
        expiresIn:
          description: >-
            Seconds the single-use handle stays redeemable (120). 0 when no
            handle is issued (provider none). Not the signed-in session's
            lifetime, which is 30 minutes from mint
          type: integer
        formSessionHandle:
          description: Absent only for a none form requested with no user
          type: string
        provider:
          enum:
            - microsoft
            - google
            - portal
            - none
          type: string
      required:
        - expiresIn
        - provider
        - embedUrl
      type: object
    SessionError:
      description: >
        A form-session refusal. `error.code` is from a closed set and each code
        keeps its HTTP status between releases; branch on the pair. `retryable`
        is true only when the same request can succeed later. `message` is for
        an operator, not an end user.


        | code | status | retryable | meaning |

        |---|---|---|---|

        | `invalid_request` | 400 | no | The request is malformed or incomplete
        — e.g. a missing `userIdentifier` or tenant for a microsoft form |

        | `invalid_api_key` | 401 | no | The API key is missing, malformed,
        revoked or expired |

        | `rate_limited` | 429 | yes | Too many requests for this key — wait
        `Retry-After` seconds |

        | `form_not_found` | 404 | no | No such form in this organisation (a
        deleted Studio form is not found) |

        | `form_unpublished` | 400 | no | Core only: the form is not available
        publicly (disabled or internal-only) |

        | `form_session_unsupported` | 400 | no | The form's authType cannot
        take a session (`password` or `internal`) |

        | `client_not_found` | 404 | no | `clientId` is not one of this
        organisation's clients |

        | `client_not_connected` | 404 | no | The client has no Microsoft tenant
        configured, so a microsoft session cannot be resolved for it |

        | `client_not_entitled` | 400 | no | Core only: the client's override
        makes the form unavailable publicly for that client |

        | `tenant_not_connected` | 403 | no | The user's Microsoft tenant (or
        Google Workspace domain) is not a client this organisation manages |

        | `user_not_found` | 404 | no | The user does not exist in that client's
        directory — invite them, then retry |

        | `invalid_user_identifier` | 400 | no | Microsoft or Google rejected
        the `userIdentifier` — give an object ID, UPN or Workspace email |

        | `microsoft_permission_denied` | 403 | no | The organisation's
        Microsoft connection is not permitted to read this user — fix the
        connection's permissions |

        | `microsoft_unavailable` | 503 | yes | Microsoft is throttling or
        temporarily unavailable — retry after `Retry-After` seconds |

        | `microsoft_connection_failed` | 500 | no | The organisation's
        Microsoft connection itself isn't working (not configured, consent
        lapsed, token refused) — an administrator must reconnect it |

        | `internal_error` | 500 | yes | An unexpected error on Triggr's side |

        | `forbidden` | 403 | no | Catch-all: refused for a reason no specific
        code covers yet |

        | `not_found` | 404 | no | Catch-all: something the request names was
        not found |

        | `unavailable` | 503 | yes | Catch-all: a dependency is unavailable |

        | `unknown` | — | no | Catch-all for any other refusal. New causes
        appear as a new code — the field is never omitted |
      properties:
        error:
          properties:
            code:
              enum:
                - invalid_request
                - invalid_api_key
                - rate_limited
                - form_not_found
                - form_unpublished
                - form_session_unsupported
                - client_not_found
                - client_not_connected
                - client_not_entitled
                - tenant_not_connected
                - user_not_found
                - invalid_user_identifier
                - microsoft_permission_denied
                - microsoft_unavailable
                - microsoft_connection_failed
                - internal_error
                - forbidden
                - not_found
                - unavailable
                - unknown
              type: string
            message:
              type: string
            retryable:
              type: boolean
          required:
            - code
            - message
            - retryable
          type: object
      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.