Skip to main content

Intent Verification

Verify a user’s identity by sending them a magic link or OTP code. Get the result via webhook or a signed JWT on redirect.

Flow

1. POST /workspaces/{id}/children        → create org (once)
2. POST /apps/{app_id}/m2m/token         → get server token (once)
3. POST /verify/{app_id}/verifications/intent  → send verification
4. User clicks link or enters code
5. You get: webhook + redirect with signed JWT

Setup

Create a child workspace

One per organization. Returns API credentials.
curl -X POST https://api.scute.io/v1/workspaces/{workspace_id}/children \
  -H "Authorization: Bearer {workspace_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme IT",
    "branding_display_name": "Acme IT",
    "branding_color": "#0c2340",
    "branding_logo_url": "https://example.com/logo.png",
    "countries": ["us", "ca"],
    "magic_link_expiry": 900,
    "webhook_url": "https://your-app.com/webhooks/scute"
  }'
Returns app_id and api_key_token. Save both. Skip this if you’re using a single app — just use your existing credentials.

Get an M2M token

curl -X POST https://api.scute.io/v1/apps/{app_id}/m2m/token \
  -H "Authorization: Bearer {api_key_token}" \
  -H "Content-Type: application/json" \
  -d '{"client_name": "my-backend"}'
Use the returned short_token (scm2m_...) in X-Authorization for all verification requests.

Send a verification

POST /v1/verify/{app_id}/verifications/intent
X-Authorization: Bearer {short_token}
FieldRequiredDescription
intent_nameyese.g. “Password Reset”, “Approve Ticket”
methodnoemail (default) or sms
verification_typenomagic_link or otp
redirect_urlnoWhere to send the user after verification
meta_data.contact_emailfor emailRecipient email
meta_data.contact_phonesfor sms[{ "phone_number": "+15551234567", "phone_type": "mobile" }]
meta_data.contact_namenoDisplay name
meta_data.ticket_idnoYour reference ID — returned in webhooks and JWT
meta_data.contact_idnoYour user ID — returned in webhooks and JWT
Add any extra fields to meta_data — they’re all echoed back.
await fetch(`https://api.scute.io/v1/verify/${APP_ID}/verifications/intent`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Authorization": `Bearer ${M2M_TOKEN}`,
  },
  body: JSON.stringify({
    intent_name: "Password Reset",
    method: "email",
    verification_type: "magic_link",
    redirect_url: "https://your-app.com/verified",
    meta_data: {
      contact_email: "user@company.com",
      contact_name: "Sarah",
      ticket_id: "12345",
    },
  }),
});

SMS OTP

await fetch(`https://api.scute.io/v1/verify/${APP_ID}/verifications/intent`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Authorization": `Bearer ${M2M_TOKEN}`,
  },
  body: JSON.stringify({
    intent_name: "Confirm Payment",
    method: "sms",
    verification_type: "otp",
    meta_data: {
      contact_email: "user@company.com",
      contact_phones: [{ phone_number: "+15551234567", phone_type: "mobile" }],
    },
  }),
});

Getting the result

Option A: Redirect with JWT

After verification, the user is redirected to:
https://your-app.com/verified?scute_token=eyJ...
The scute_token is an RS256 JWT signed with your app’s key. Verify it against your JWKS:
GET /v1/auth/{app_id}/jwks
Payload:
{
  "sub": "user@company.com",
  "verification_id": "uuid",
  "intent": "Password Reset",
  "ticket_id": "12345",
  "contact_id": "1991",
  "verified_at": 1776316476,
  "exp": 1776316536,
  "iss": "scute",
  "aud": "app_id"
}
60-second TTL. One-time use. Redirect URL template — set a default in Settings with placeholders:
https://your-app.com/verified?ticket={ticket_id}&contact={contact_id}
The scute_token is appended automatically.

Option B: Webhook

Events fire to your registered webhook URL.
EventWhen
verification.*.requestedSent
verification.*.verifiedConfirmed
verification.*.expiredTimed out
verification.*.failedOTP max attempts
verification.*.deniedContact denied (consent mode)
verification.*.delivery_failedSMS/email didn’t send
Replace * with email or sms. Payload:
{
  "event_type": "verification.sms.verified",
  "data": {
    "verification_id": "uuid",
    "intent": "Password Reset",
    "meta_data": { "ticket_id": "12345", "contact_email": "user@company.com" },
    "timing": {
      "sent_at": "...",
      "confirmed_at": "...",
      "time_to_verify_seconds": 52
    },
    "device": { "os": "iOS", "browser": "Safari" },
    "ip_address": "203.0.113.42"
  }
}
Expiration events include was_opened: true/false so you know if the link was ever clicked. Signature: X-Webhook-Signature: t={timestamp},v1={hmac} — verify with HMAC-SHA256("{timestamp}.{payload}", webhook_secret).

Verification modes

Set in Settings > Verification. Dismiss (default) — confirm identity, redirect. Done. Consent — confirm identity, then ask the user to approve or deny the action. Webhook includes consent_decision: "approved" or "denied".

Managing verifications

ActionEndpoint
Check statusGET /v1/verify/{app_id}/verifications/{id}
CancelDELETE /v1/verify/{app_id}/verifications/{id}
ResendPOST /v1/verify/{app_id}/verifications/{id}/resend
DenyPOST /v1/verify/{app_id}/verifications/{id}/deny

Settings

SettingDefault
Tenant modefull or verify_only
Verification modedismiss or consent
Redirect URL template
Redirect delay4s
Success message”Your identity has been successfully verified.”
Rate limit10/hour per contact

Testing

Scute Testbench — connect with M2M token, search users, send verifications, watch webhooks live. Local webhooks: ngrok http 3000