Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.scute.io/llms.txt

Use this file to discover all available pages before exploring further.

Every event you subscribe to is delivered as a POST to your endpoint with the same top-level shape. The two fields most integrations care about are event_type (what happened) and metadata (what you tagged the verification with at create time).

Headers

POST <your_url>
Content-Type: application/json
User-Agent: AppName-Webhooks/1.0
X-Webhook-Signature: t=<unix_timestamp>,v1=<hmac_sha256_hex>
The signature is HMAC-SHA256 of "<timestamp>.<raw_body>" using your endpoint’s secret. Recompute it on your side and reject the request if it doesn’t match.

Body

{
  "id": "<delivery_uuid>",
  "verification_id": "<challenge_uuid>",
  "challenge_id": "<challenge_uuid>",
  "created_at": "2026-05-27T02:33:33Z",
  "event_type": "verification.success",
  "app_id": "app_l3RxEBGbQq5Q0b6T6K6N",
  "user": {
    "id": "<user_uuid>",
    "app_user_id": "<app_user_uuid>",
    "external_id": "your-own-id-or-null",
    "email": "jane@acme.com",
    "phone": "+15555550100",
    "msp_id": "msp_xxx"
  },
  "metadata": {
    "ticket_id": "T-123",
    "company_id": "C-9",
    "msp": {
      "id": "msp_xxx",
      "msp_client_app_id": "app_clientxxx",
      "msp_client_workspace_id": "<workspace_uuid>"
    }
  },
  "data": {
    "purpose": "verify_contact",
    "method": "magic_link",
    "outcome": "verified",
    "intent": "Refund approval",
    "attempts": 0
  },
  "api_version": "v1"
}

Top-level fields

FieldTypeNotes
idstring (uuid)The webhook delivery ID. Unique per delivery attempt — use it for idempotency keys.
verification_idstring (uuid)The challenge UUID that triggered the event. Same as challenge_id.
challenge_idstring (uuid)Alias for verification_id. Either works for lookups.
created_atISO 8601When this delivery was created (not when the underlying event happened).
event_typestringThe event slug. See below.
app_idstringPublic app ID (app_xxx) the event belongs to.
userobject | omittedThe verifying user. Omitted for system events with no user.
metadataobject | omittedWhatever you passed in metadata at challenge create time, plus any auto-attached info (e.g. msp for MSP challenges). Omitted when empty.
dataobjectEvent-type-specific fields (purpose, method, outcome, etc.).
api_versionstringWebhook endpoint API version.

user block

FieldTypeNotes
idstring (uuid)Scute’s User ID (cross-app, identifies the human).
app_user_idstring (uuid)Scute’s AppUser ID (scoped to this app).
external_idstring | nullThe ID you supplied at user creation, if any. Use this to map back to your own DB.
emailstring | nullPrimary email.
phonestring | nullPrimary phone (E.164).
msp_idstring | nullPublic ID of the MSP this user belongs to (msp_xxx format). Omitted when the user’s workspace isn’t tagged to an MSP.

metadata block

Everything you passed in metadata when you created the challenge, echoed back verbatim. Use it as the primary correlation key — ticket_id, order_id, whatever your domain needs. For MSP challenges (created via POST /v1/workspaces/:workspace_id/challenges), Scute auto-attaches an msp sub-object with routing identifiers:
"msp": {
  "id": "msp_xxx",
  "msp_client_app_id": "app_clientxxx",
  "msp_client_workspace_id": "<workspace_uuid>"
}
Cosmetic MSP fields (name, logo_url, primary_color) are used by the tenant verifier page but not included in webhook payloads — your handler gets only the routing IDs. The _plain_code field (used internally for OTP delivery) is always stripped from the payload — it is never sent to your endpoint.

Event types

Subscribe via the dashboard or POST /v1/apps/:app_id/webhook_endpoints. The slugs we currently emit:

Verification

SlugWhen
verification.attemptedA challenge was created and delivery started.
verification.successUser completed verification (correct code, magic link clicked, passkey approved).
verification.failedWrong code / max attempts exceeded.
verification.deniedUser explicitly denied/rejected (consent mode).
verification.email.requestedA new email verification was requested.

User

SlugWhen
user.createdNew user account created via the management API.
user.updatedUser profile fields changed.
user.deletedUser hard-deleted.
user.meta.updateduser_meta fields changed.
user.status.changedActive ⇄ inactive.
user.invitedMagic-link invite sent.

Auth / sessions

SlugWhen
magiclink.login.initMagic link login initiated.
magiclink.auth.successMagic link authenticated.
otp.send.initOTP sent.
otp.verify.success / otp.verify.failedOTP verification outcome.
session.magic.created / session.otp.created / session.webauthn.createdSession minted.

App / system

SlugWhen
app.created / app.updated / app.deletedApp lifecycle.
webhook.testManual test fired from the dashboard.

Wildcard

SlugWhen
*Subscribe to everything.

Delivery semantics

  • At-least-once. If your endpoint returns non-2xx, we retry with backoff up to the endpoint’s retry_limit (default 3). Use the id field for idempotency.
  • Exactly one POST per event per matching subscriber. No duplicates.
  • No ordering guarantee. Don’t assume events arrive in causal order.
  • Async delivery. Webhook delivery happens shortly after the underlying event — typically within milliseconds, but can be delayed under load.
  • No automatic IP allowlisting today. Validate via the X-Webhook-Signature header.

Verifying the signature (example, Node.js)

import crypto from "crypto";

function verify(rawBody, headerValue, secret) {
  const [tsPart, sigPart] = headerValue.split(",");
  const timestamp = tsPart.replace("t=", "");
  const expected = sigPart.replace("v1=", "");
  const computed = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(expected));
}
Pass the raw request body (not the parsed JSON object) when computing the signature.

Per-challenge callback_url (alternative to webhook endpoints)

If you set callback_url when creating a challenge, Scute also POSTs a single request to that URL when the challenge reaches a terminal state. The payload shape differs from the webhook endpoint payload — see Manage Users for the challenge creation API. Use webhook endpoints unless you specifically need per-challenge routing.