# Create API key Source: https://docs.scute.io/api-reference/api-keys/create-api-key /swagger.json post /v1/apps/{app_id}/api_keys Create a new API key for the app # Delete API key Source: https://docs.scute.io/api-reference/api-keys/delete-api-key /swagger.json delete /v1/apps/{app_id}/api_keys/{id} Revoke an API key # List API keys Source: https://docs.scute.io/api-reference/api-keys/list-api-keys /swagger.json get /v1/apps/{app_id}/api_keys Retrieve all API keys associated with an app # Show API key Source: https://docs.scute.io/api-reference/api-keys/show-api-key /swagger.json get /v1/apps/{app_id}/api_keys/{id} Retrieve details for an individual API key # Update API key Source: https://docs.scute.io/api-reference/api-keys/update-api-key /swagger.json patch /v1/apps/{app_id}/api_keys/{id} Update the nickname for an existing API key # Create app Source: https://docs.scute.io/api-reference/apps/create-app /swagger.json post /v1/apps Create a new app. Requires API Secret only (app or workspace key). Workspace is resolved from: explicit workspace_id param, workspace API key bearer, or app API key's workspace. # Delete app Source: https://docs.scute.io/api-reference/apps/delete-app /swagger.json delete /v1/apps/{id} Soft-delete an app. Requires API Secret only. # List apps Source: https://docs.scute.io/api-reference/apps/list-apps /swagger.json get /v1/apps Get all apps for a workspace. Requires API Secret (app or workspace). Pass workspace via X-Workspace header or workspace_id param. # Show app Source: https://docs.scute.io/api-reference/apps/show-app /swagger.json get /v1/apps/{id} Get public app configuration. No authentication required. # Update app Source: https://docs.scute.io/api-reference/apps/update-app /swagger.json patch /v1/apps/{id} Update an existing app. Requires both API Secret and User Access Token. # Update App Settings Source: https://docs.scute.io/api-reference/apps/update-app-settings configure an app's authentication settings Update authentication settings for an app. ## Endpoint ``` PATCH /v1/apps/:app_id ``` ## Authentication Send your workspace (or app-scoped) API secret as a Bearer token. ```http theme={null} Authorization: Bearer Content-Type: application/json ``` The bearer must own the app: * **Workspace key** (`scwor_…`) — can manage any app in the workspace * **App key** (`scapp_…`) — can only manage its own app Wrong scope returns `403 "API key not authorized for this app"`. ## Body Wrap fields in an `app` object. All fields are optional. send only what you want to change. ### Token lifetimes All values are in **seconds**. | Field | Default | Typical range | Description | | ----------------------- | -------- | --------------- | --------------------------------- | | `access_expiration` | `3600` | 60 – 604800 | Access token TTL | | `refresh_expiration` | `604800` | 3600 – 31536000 | Refresh token TTL | | `magic_link_expiration` | `2000` | 60 – 604800 | Magic link TTL | | `otp_expiration` | `600` | 30 – 3600 | OTP code TTL | | `session_timeout` | `604800` | — | Session inactivity timeout | | `auto_refresh` | `true` | bool | Auto-refresh access tokens | | `refresh_payload` | `true` | bool | Include payload in refresh tokens | ### Other common fields `name`, `origin`, `additional_origins[]`, `mfa_policy`, `mfa_methods_allowed[]`, `email_required`, `phone_number_required`, `passkeys_enabled`, `email_auth_type` (`magic` or `otp`), `otp_length`. ## Example ```bash theme={null} curl -X PATCH https://api.scute.io/v1/apps/app_l3RxEBGbQq5Q0b6T6K6N \ -H "Authorization: Bearer $SCUTE_API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "app": { "access_expiration": 7200, "refresh_expiration": 1209600 } }' ``` ## Response — `200 OK` ```json theme={null} { "app": { "id": "app_l3RxEBGbQq5Q0b6T6K6N", "name": "...", "access_expiration": 7200, "refresh_expiration": 1209600, "magic_link_expiration": 2000, "otp_expiration": 600, "session_timeout": 604800, "auto_refresh": true, "...": "..." } } ``` Changes take effect on the **next token issued**, existing tokens keep their original expiration until they expire naturally. ## Errors | Status | When | | ------ | --------------------------------------------------------------------- | | `401` | Missing or invalid API key | | `403` | API key isn't authorized for this app (wrong workspace) | | `404` | App not found | | `422` | Validation error (e.g. malformed `origin`) — error message in `error` | # List auth providers Source: https://docs.scute.io/api-reference/auth-providers/list-auth-providers /swagger.json get /v1/apps/{app_id}/auth_providers Retrieve merged list of email, oauth, and message auth providers for an app # Upsert auth provider Source: https://docs.scute.io/api-reference/auth-providers/upsert-auth-provider /swagger.json post /v1/apps/{app_id}/auth_providers Create or update an auth provider for the app # Cancel a challenge Source: https://docs.scute.io/api-reference/challenges/cancel-a-challenge /swagger.json delete /v1/auth/{app_id}/challenges/{token} Cancel an active challenge. This is a soft cancellation - the challenge will be marked as cancelled but not deleted. # Create a challenge Source: https://docs.scute.io/api-reference/challenges/create-a-challenge /swagger.json post /v1/auth/{app_id}/challenges Create a new challenge for authentication, MFA, step-up verification, or custom flows. **Purposes:** - `authenticate` - Primary login verification (returns session tokens on success) - `mfa` - Second factor after primary auth (returns session tokens on success) - `step_up` - Re-verify identity for sensitive actions - `verify_contact` - Verify email/phone ownership - `verify_identity` - KYC/identity verification - `change_identifier` - Verify new email/phone before updating - `custom` - Custom verification with callback **Methods:** - `email_otp` - Send OTP code via email - `sms_otp` - Send OTP code via SMS - `magic_link` - Send magic link via email - `totp` - Time-based OTP (authenticator app) - `backup_code` - Use backup recovery code - `webauthn` - Passkey/security key verification - `push` - Push notification approval # Deny a challenge Source: https://docs.scute.io/api-reference/challenges/deny-a-challenge /swagger.json post /v1/auth/{app_id}/challenges/{token}/deny Explicitly deny/reject a challenge. Used for push-based approvals or when the user wants to reject a verification request. If a callback URL was provided when creating the challenge, it will be notified of the denial. # Get challenge status Source: https://docs.scute.io/api-reference/challenges/get-challenge-status /swagger.json get /v1/auth/{app_id}/challenges/{token} Retrieve the current status of a challenge. Use this to poll for challenge state or display remaining time/attempts to the user. # Resend challenge Source: https://docs.scute.io/api-reference/challenges/resend-challenge /swagger.json post /v1/auth/{app_id}/challenges/{token}/resend Resend a challenge delivery (OTP code or magic link). This cancels the current challenge and creates a new one with a fresh code. Only applicable to delivery-based methods: `email_otp`, `sms_otp`, `magic_link` # Verify a challenge Source: https://docs.scute.io/api-reference/challenges/verify-a-challenge /swagger.json post /v1/auth/{app_id}/challenges/{token}/verify Submit verification for a challenge. **For OTP/backup code verification:** Send `code` parameter **For WebAuthn verification:** Send `credential` parameter with assertion On successful verification: - `authenticate` and `mfa` challenges return session tokens - `step_up`, `verify_contact`, `custom` challenges return completion status - If MFA is required after authentication, returns `mfa_required: true` with a new MFA challenge # List events Source: https://docs.scute.io/api-reference/events/list-events /swagger.json get /v1/apps/{app_id}/events Retrieve paginated list of events for an application # Create M2M access token Source: https://docs.scute.io/api-reference/m2m-sessions/create-m2m-access-token /swagger.json post /v1/apps/{app_id}/m2m/token Create a machine-to-machine session and return long-lived JWT credentials # List all M2M sessions for an app Source: https://docs.scute.io/api-reference/m2m-sessions/list-all-m2m-sessions-for-an-app /swagger.json get /v1/apps/{app_id}/m2m/sessions Retrieve all machine-to-machine sessions created for the given app # Refresh the M2M access token Source: https://docs.scute.io/api-reference/m2m-sessions/refresh-the-m2m-access-token /swagger.json post /v1/apps/{app_id}/m2m/refresh Rotate the access token for the active machine-to-machine session using the refresh token # Revoke a specific M2M session Source: https://docs.scute.io/api-reference/m2m-sessions/revoke-a-specific-m2m-session /swagger.json delete /v1/apps/{app_id}/m2m/sessions/{session_id} Delete a machine-to-machine session by ID using API key authentication # Revoke the current M2M session Source: https://docs.scute.io/api-reference/m2m-sessions/revoke-the-current-m2m-session /swagger.json delete /v1/apps/{app_id}/m2m/revoke Invalidate the caller's active machine-to-machine session # Verify the active M2M session Source: https://docs.scute.io/api-reference/m2m-sessions/verify-the-active-m2m-session /swagger.json get /v1/apps/{app_id}/m2m/verify Validate the caller's JWT belongs to an active machine-to-machine session # Authenticate with magic link Source: https://docs.scute.io/api-reference/magic-links/authenticate-with-magic-link /swagger.json patch /v1/auth/{app_id}/magic_links/authenticate Authenticate a magic link token and return session tokens. This endpoint validates the magic link token and creates a new session for the user. # Check magic link status Source: https://docs.scute.io/api-reference/magic-links/check-magic-link-status /swagger.json post /v1/auth/{app_id}/magic_links/status Poll to check if a magic link has been activated. Once activated, this endpoint returns authentication tokens. This enables cross-device login flows where the user initiates login on one device and completes it on another. # Confirm workspace invitation Source: https://docs.scute.io/api-reference/magic-links/confirm-workspace-invitation /swagger.json post /v1/auth/{app_id}/magic_links/confirm_invite Confirm a workspace invitation using magic link token and optionally provide required user metadata. Returns session tokens upon successful confirmation. # Send login magic link Source: https://docs.scute.io/api-reference/magic-links/send-login-magic-link /swagger.json post /v1/auth/{app_id}/magic_links/login Send a login magic link email to the user. The user will receive an email with a link to complete their login. If the email doesn't exist, a new user will be created. # Send registration magic link Source: https://docs.scute.io/api-reference/magic-links/send-registration-magic-link /swagger.json post /v1/auth/{app_id}/magic_links/register Send a registration magic link email to the user. The user will receive an email with a link to complete their registration. # List message service providers Source: https://docs.scute.io/api-reference/message-service-providers/list-message-service-providers /swagger.json get /v1/apps/{app_id}/message_service_providers Retrieve the configured message delivery providers for the specified app # Update message service provider Source: https://docs.scute.io/api-reference/message-service-providers/update-message-service-provider /swagger.json patch /v1/apps/{app_id}/message_service_providers Update an existing message service provider configuration # Upsert message service provider Source: https://docs.scute.io/api-reference/message-service-providers/upsert-message-service-provider /swagger.json post /v1/apps/{app_id}/message_service_providers Create or update a message service provider configuration for the app # Dismiss notification Source: https://docs.scute.io/api-reference/notifications/dismiss-notification /swagger.json post /v1/apps/{app_id}/notifications/{id}/dismiss Dismiss a notification (only works for dismissible notifications) # Get notification Source: https://docs.scute.io/api-reference/notifications/get-notification /swagger.json get /v1/apps/{app_id}/notifications/{id} Retrieve a single notification by ID # Get notification statistics Source: https://docs.scute.io/api-reference/notifications/get-notification-statistics /swagger.json get /v1/apps/{app_id}/notifications/stats Get notification statistics for the authenticated user including counts by priority and category # List notifications Source: https://docs.scute.io/api-reference/notifications/list-notifications /swagger.json get /v1/apps/{app_id}/notifications Retrieve paginated list of notifications for the authenticated user # Mark all notifications as read Source: https://docs.scute.io/api-reference/notifications/mark-all-notifications-as-read /swagger.json post /v1/apps/{app_id}/notifications/mark_all_read Mark all unread notifications as read for the authenticated user # Mark notification as read Source: https://docs.scute.io/api-reference/notifications/mark-notification-as-read /swagger.json post /v1/apps/{app_id}/notifications/{id}/mark_read Mark a single notification as read # Initiate OAuth authorization Source: https://docs.scute.io/api-reference/oauth/initiate-oauth-authorization /swagger.json get /v1/auth/{app_id}/oauth/authorize Initiate OAuth authorization flow by redirecting to the provider's authorization endpoint. The provider must be configured for the app. # OAuth callback handler Source: https://docs.scute.io/api-reference/oauth/oauth-callback-handler /swagger.json get /v1/auth/{app_id}/oauth/callback Handle OAuth callback from provider. Exchanges authorization code for access token, fetches user info, creates or finds user, and redirects to app with a magic link token for authentication. # App Source: https://docs.scute.io/api-reference/objects/app ## The App Object Apps are applications within a workspace. Each app has its own authentication configuration, users, and API keys. ### Attributes Unique identifier for the app **Example:** `app_123` App name **Example:** `My Application` URL-friendly app identifier **Example:** `my-application` App origin URL for CORS and OAuth redirects **Example:** `https://myapp.com` OAuth callback URL **Example:** `https://myapp.com/auth/callback` URL to the app logo image **Example:** `https://example.com/app-logo.png` ISO 8601 timestamp when the app was created ISO 8601 timestamp when the app was last updated ## Example App Object ```json theme={null} { "id": "app_123", "name": "My Application", "slug": "my-application", "origin": "https://myapp.com", "callback_url": "https://myapp.com/auth/callback", "logo": "https://example.com/app-logo.png", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ``` # Challenge Source: https://docs.scute.io/api-reference/objects/challenge ## The Challenge Object A challenge is a single verification a user has to complete — an OTP, a magic link, a passkey prompt, etc. One challenge tracks one attempt at proving something: that the user owns this email, that they're really the one signing in, that they consent to a sensitive action. ### Attributes Unique identifier. The app this challenge belongs to. The user being challenged. May be empty for anonymous flows (e.g. a sign-up before the user record exists). Why this challenge exists. One of: * `authenticate` — primary login * `mfa` — second factor after primary auth * `step_up` — re-verify before a sensitive action * `verify_contact` — confirm an email or phone number * `verify_identity` — KYC / identity verification * `change_identifier` — confirm a new email or phone before swapping * `custom` — triggered by you, for your own flow How the user will prove themselves. One of: `email_otp`, `sms_otp`, `magic_link`, `totp`, `backup_code`, `webauthn`, `oauth`, `push`, `plaid_idv` Current state. One of: * `pending` — issued, waiting on the user * `completed` — user passed * `failed` — too many wrong attempts * `expired` — `expires_at` passed before completion * `cancelled` — voided by you or the user * `denied` — explicitly rejected (e.g. user said "this wasn't me") The email or phone the challenge was sent to, when applicable. Free-form label for what action this challenge guards (e.g. `login`, `delete_account`, `wire_transfer`). Useful for filtering and audit. Structured data describing the action. Echoed back when the challenge completes so you can act on it. Who started the challenge: `user`, `admin`, `system`, or your own value. ID of the initiator. If this challenge is a follow-up to another (e.g. MFA after primary auth), the parent's ID. Number of guesses made so far. How many guesses are allowed before the challenge fails. Default `3`. `max_attempts - attempts`. Convenience field. Lifetime in seconds from creation. Default `600`. Where the user is sent after completing a magic link or OAuth challenge. Shareable short URL for magic-link and similar flows. Only present once a short token has been minted. Arbitrary key/value data you attached when creating the challenge. Additional context captured by Scute (request origin, flow hints, etc.). User agent / device fingerprint of the requester, when available. IP that initiated the challenge. ISO 8601 timestamp. When the challenge stops accepting answers. When the OTP / magic link was successfully sent. When the user first opened the magic link or OTP message, if tracked. When the user passed the challenge. When the challenge reached its final state (verified, failed, expired, cancelled, or denied). ## Example ```json theme={null} { "id": "ch_01H8X...", "app_id": "app_01H8X...", "app_user_id": "usr_01H8X...", "purpose": "authenticate", "challenge_method": "email_otp", "status": "pending", "identifier": "user@example.com", "intent": "login", "intent_fields": {}, "initiator_type": "user", "initiator_id": "usr_01H8X...", "attempts": 0, "max_attempts": 3, "remaining_attempts": 3, "timeout": 600, "callback_url": null, "short_url": null, "metadata": {}, "context": {}, "ip_address": "203.0.113.42", "created_at": "2026-04-23T10:30:00Z", "expires_at": "2026-04-23T10:40:00Z", "delivered_at": "2026-04-23T10:30:01Z", "opened_at": null, "verified_at": null, "completed_at": null } ``` # Event Source: https://docs.scute.io/api-reference/objects/event ## The Event Object Events represent activities and actions within your application. They can trigger webhooks and are used for audit logging. ### Attributes Unique identifier for the event **Example:** `evt_123` Application ID where the event occurred Event type identifier **Example:** `user.created` Event category for grouping related events **Example:** `user` Originating controller that triggered the event **Example:** `UsersController` Originating action that triggered the event **Example:** `create` ID of the user associated with this event Event payload containing relevant data **Example:** `{ "user_id": "usr_123", "email": "user@example.com" }` Additional metadata about the event **Example:** `{ "ip_address": "192.168.1.1", "user_agent": "Mozilla/5.0" }` Captured request parameters Optional note or description IP address where the event originated **Example:** `192.168.1.1` Browser or client user agent string **Example:** `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)` ISO 8601 timestamp when the event was created ISO 8601 timestamp when the event was last updated ## Example Event Object ```json theme={null} { "id": "evt_123", "app_id": "app_123", "slug": "user.created", "category": "user", "controller": "UsersController", "action": "create", "app_user_id": "usr_456", "data": { "user_id": "usr_456", "email": "user@example.com", "name": "John Doe" }, "metadata": { "source": "api", "version": "v1" }, "params": {}, "ip_address": "192.168.1.1", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ``` # Scute Session Source: https://docs.scute.io/api-reference/objects/scute-session ## The Scute Session Object Scute Session represents the authentication tokens returned after successful authentication. This object contains JWT tokens used to authenticate API requests. ### Attributes JWT access token for API authentication **Example:** `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...` JWT refresh token for obtaining new access tokens (only included if app has refresh\_payload enabled) **Example:** `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...` CSRF token for request validation **Example:** `a1b2c3d4e5f6g7h8i9j0` Unix timestamp when the access token expires **Example:** `1705318200` Unix timestamp when the refresh token expires (only included if refresh token is present) **Example:** `1737940200` ID of the authenticated user (not included for M2M sessions) **Example:** `usr_1234567890` ID of the RSA key used to sign the JWT tokens **Example:** `key_abc123` Client name for M2M (machine-to-machine) sessions only **Example:** `api-service-prod` ## Example Scute Session Object ### User Session (with refresh token) ```json theme={null} { "access": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoidXNyXzEyMzQ1Njc4OTAiLCJhaWQiOiJhcHBfMTIzIiwid2lkIjoid3NfMTIzIiwiZXhwIjoxNzA1MzE4MjAwfQ...", "refresh": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoidXNyXzEyMzQ1Njc4OTAiLCJhaWQiOiJhcHBfMTIzIiwid2lkIjoid3NfMTIzIiwiZXhwIjoxNzM3OTQwMjAwfQ...", "csrf": "a1b2c3d4e5f6g7h8i9j0", "access_expires_at": 1705318200, "refresh_expires_at": 1737940200, "user_id": "usr_1234567890", "key_id": "key_abc123" } ``` ### User Session (without refresh token) ```json theme={null} { "access": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoidXNyXzEyMzQ1Njc4OTAiLCJhaWQiOiJhcHBfMTIzIiwid2lkIjoid3NfMTIzIiwiZXhwIjoxNzA1MzE4MjAwfQ...", "csrf": "a1b2c3d4e5f6g7h8i9j0", "access_expires_at": 1705318200, "user_id": "usr_1234567890", "key_id": "key_abc123" } ``` ### M2M (Machine-to-Machine) Session ```json theme={null} { "access": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhaWQiOiJhcHBfMTIzIiwid2lkIjoid3NfMTIzIiwibTJtIjp0cnVlLCJleHAiOjQ4NjExODIwMDB9...", "refresh": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhaWQiOiJhcHBfMTIzIiwid2lkIjoid3NfMTIzIiwibTJtIjp0cnVlLCJleHAiOjQ4NjExODIwMDB9...", "csrf": "a1b2c3d4e5f6g7h8i9j0", "access_expires_at": 4861182000, "refresh_expires_at": 4861182000, "client_name": "api-service-prod", "key_id": "key_abc123" } ``` ## JWT Token Payload The access and refresh tokens contain the following claims: User ID (only in user sessions, not in M2M sessions) Application ID Workspace ID Flag indicating this is a machine-to-machine session (only in M2M tokens) Credential ID (only in WebAuthn sessions) Token expiration time (Unix timestamp) Unique token identifier for session management # User Source: https://docs.scute.io/api-reference/objects/user ## The User Object Users represent end-users in your application. Each user can have multiple authentication methods and custom metadata. ### Attributes Unique identifier for the user (app user ID) **Example:** `usr_1234567890` Application ID this user belongs to Base user ID (platform-level identifier) User status. Can be `active`, `pending`, `inactive`, or `imported` **Example:** `active` User email address **Example:** `user@example.com` User phone number **Example:** `+1234567890` User full name **Example:** `John Doe` Whether the user's email has been verified **Example:** `true` Whether the user's phone has been verified **Example:** `false` Number of times the user has logged in **Example:** `42` ISO 8601 timestamp of the user's last login Custom metadata object with user-defined fields **Example:** `{ "first_name": "John", "last_name": "Doe", "company": "Acme Corp" }` ISO 8601 timestamp when the user was created ISO 8601 timestamp when the user was last updated ## Example User Object ```json theme={null} { "id": "usr_1234567890", "app_id": "app_123", "user_id": "user_abc", "status": "active", "email": "user@example.com", "phone": "+1234567890", "name": "John Doe", "email_verified": true, "phone_verified": false, "login_count": 42, "last_login_at": "2024-01-15T10:30:00Z", "user_meta": { "first_name": "John", "last_name": "Doe", "company": "Acme Corp", "role": "Developer" }, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ``` # Workspace Source: https://docs.scute.io/api-reference/objects/workspace ## The Workspace Object Workspaces are the top-level organizational unit in Scute. Each workspace can contain multiple apps and has its own members with different roles. ### Attributes Unique identifier for the workspace **Example:** `ws_123` Workspace name **Example:** `Acme Corp` URL-friendly workspace identifier **Example:** `acme-corp` Whether this is a personal workspace (automatically created for each user) **Example:** `false` Current user's membership information in this workspace User's role in the workspace. Can be `owner`, `admin`, or `member` ISO 8601 timestamp when the membership was created Workspace configuration settings **Example:** `{ "onboarding": { "fullName": true, "companyName": false } }` Whether the workspace has a pro subscription **Example:** `false` Whether public signup is enabled for this workspace's apps **Example:** `false` Level of user information required during signup. Can be `none`, `email`, or `full` **Example:** `email` URL to the workspace logo image **Example:** `https://example.com/logo.png` ISO 8601 timestamp when the workspace was created ISO 8601 timestamp when the workspace was last updated ## Example Workspace Object ```json theme={null} { "id": "ws_123", "name": "Acme Corp", "slug": "acme-corp", "personal": false, "membership": { "role": "owner", "created_at": "2024-01-15T10:30:00Z" }, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z", "settings": { "onboarding": { "fullName": true, "companyName": false } }, "is_pro": false, "public_signup": false, "user_info_requirement": "email", "logo_url": "https://example.com/logo.png" } ``` # Send OTP code Source: https://docs.scute.io/api-reference/otp/send-otp-code /swagger.json post /v1/auth/{app_id}/otps/login Send a one-time passcode to the user's phone number via SMS or email. Creates a new user if the identifier doesn't exist. # Verify OTP code Source: https://docs.scute.io/api-reference/otp/verify-otp-code /swagger.json post /v1/auth/{app_id}/otps/verify Verify a one-time passcode and return session tokens. The user_id parameter is the challenge token returned from the /otps/login endpoint. # Get JWKS (JSON Web Key Set) for JWT verification Source: https://docs.scute.io/api-reference/rsa-keys/get-jwks-json-web-key-set-for-jwt-verification /swagger.json get /v1/auth/{app_id}/jwks Returns the public key in JWKS format for JWT token verification. This endpoint is PUBLIC and requires no authentication. **JWKS Compliance:** - Compliant with RFC 7517 (JSON Web Key specification) - Standard format expected by JWT libraries - Includes RSA modulus (n) and exponent (e) for verification **Why this endpoint is public:** - Industry standard for JWT verification endpoints - Required by OAuth2/OIDC specifications - Enables automatic key discovery by client libraries The endpoint delegates to the app's workspace key infrastructure. # Get RSA public key for JWT verification Source: https://docs.scute.io/api-reference/rsa-keys/get-rsa-public-key-for-jwt-verification /swagger.json get /v1/auth/{app_id}/public_key Returns the RSA public key for JWT token verification. This endpoint is PUBLIC and requires no authentication. **Why this endpoint is public:** - Required for JWT verification by client applications - Follows OAuth2/OIDC standards where JWKS endpoints are public - Avoids chicken-and-egg problem (clients need keys to verify tokens) - Only exposes public keys, no sensitive data The endpoint delegates to the app's workspace key infrastructure. # Get current user Source: https://docs.scute.io/api-reference/session-management/get-current-user /swagger.json get /v1/auth/{app_id}/current_user Get detailed profile information for the currently authenticated user. Requires a valid access token. # List all sessions Source: https://docs.scute.io/api-reference/session-management/list-all-sessions /swagger.json get /v1/auth/{app_id}/sessions List all active sessions for the currently authenticated user. Requires a valid access token. # Revoke session Source: https://docs.scute.io/api-reference/session-management/revoke-session /swagger.json delete /v1/auth/{app_id}/sessions/{id} Revoke a specific session by ID for the authenticated user. This will invalidate the session and require re-authentication. Requires a valid access token. # Sign out current user Source: https://docs.scute.io/api-reference/session-management/sign-out-current-user /swagger.json delete /v1/auth/{app_id}/current_user Sign out the current user and invalidate their session. Requires a valid access token. # Update current user metadata Source: https://docs.scute.io/api-reference/session-management/update-current-user-metadata /swagger.json patch /v1/auth/{app_id}/current_user/meta Update the metadata for the currently authenticated user. Requires a valid access token. # Refresh access token Source: https://docs.scute.io/api-reference/tokens/refresh-access-token /swagger.json post /v1/auth/{app_id}/tokens/refresh Refresh the access token using a valid refresh token. The refresh token must be provided in the Authorization header. # Verify access token Source: https://docs.scute.io/api-reference/tokens/verify-access-token /swagger.json post /v1/auth/{app_id}/tokens/verify/access Verify that an access token is valid and not expired. Requires a valid access token in the Authorization header. # Verify refresh token Source: https://docs.scute.io/api-reference/tokens/verify-refresh-token /swagger.json post /v1/auth/{app_id}/tokens/verify/refresh Verify that a refresh token is valid and not expired. Requires a valid refresh token in the Authorization header. # Create user meta field Source: https://docs.scute.io/api-reference/user-meta-fields/create-user-meta-field /swagger.json post /v1/apps/{app_id}/user_meta_fields Create a new user meta field for the application # Delete user meta field Source: https://docs.scute.io/api-reference/user-meta-fields/delete-user-meta-field /swagger.json delete /v1/apps/{app_id}/user_meta_fields/{id} Delete a user meta field # List user meta fields Source: https://docs.scute.io/api-reference/user-meta-fields/list-user-meta-fields /swagger.json get /v1/apps/{app_id}/user_meta_fields Get all user meta fields for an application (public endpoint) # Update meta fields positions Source: https://docs.scute.io/api-reference/user-meta-fields/update-meta-fields-positions /swagger.json patch /v1/apps/{app_id}/user_meta_fields/update_positions Update the display order of user meta fields # Update user meta field Source: https://docs.scute.io/api-reference/user-meta-fields/update-user-meta-field /swagger.json put /v1/apps/{app_id}/user_meta_fields/{id} Update an existing user meta field # Activate user Source: https://docs.scute.io/api-reference/users/activate-user /swagger.json post /v1/{app_id}/users/{id}/activate Activate a deactivated user # Create user Source: https://docs.scute.io/api-reference/users/create-user /swagger.json post /v1/{app_id}/users Create a new user with email or phone identifier. Accepts app API secret or workspace API secret. # Deactivate user Source: https://docs.scute.io/api-reference/users/deactivate-user /swagger.json post /v1/{app_id}/users/{id}/deactivate Deactivate an active user # Delete user Source: https://docs.scute.io/api-reference/users/delete-user /swagger.json delete /v1/{app_id}/users/{id} Delete a user from the app # Import users Source: https://docs.scute.io/api-reference/users/import-users /swagger.json post /v1/{app_id}/users/import Bulk import multiple users from CSV/JSON data. Requires email column to be present. # Invite user Source: https://docs.scute.io/api-reference/users/invite-user /swagger.json post /v1/{app_id}/users/invite Invite a new user to the app, creates user with pending status and sends invitation email # List users Source: https://docs.scute.io/api-reference/users/list-users /swagger.json get /v1/{app_id}/users Get a paginated list of users with optional filtering. Accepts app API secret or workspace API secret (for apps in that workspace). # Manage Users Source: https://docs.scute.io/api-reference/users/manage-users Create, read, update, and delete users within an app via the User Management API. Programmatic CRUD over users in an app. ## Authentication (same for all endpoints) ```http theme={null} Authorization: Bearer Content-Type: application/json ``` The key must own the app you're calling against: * **App key** (`scapp_…`) — can only manage users in its own app * **Workspace key** (`scwor_…`) — can manage users in any app within the workspace (or any child workspace, if you're an MSP parent) Wrong scope → `403 "API key not authorized for this app"`. *** ## Create a user ``` POST /v1/:app_id/users ``` `:app_id` is the **Client App ID** (`app_…`) — the app the user should belong to. ### Body Either `email` or `phone` is required. Everything else is optional. | Field | Type | Description | | ---------------------------------------- | ---------------------- | ------------------------------------------------------- | | `email` | string | Primary email identifier | | `phone` | string | E.164 phone identifier | | `identifier` | string | Back-compat: pass either, type is auto-detected | | `first_name`, `middle_name`, `last_name` | string | Stored together as `name` | | `external_id` | string | Your own ID for cross-system mapping (unique per app) | | `user_meta` | object | Arbitrary key/value metadata | | `status` | `active` \| `inactive` | Defaults to `active` | | `send_invite` | bool | Send magic-link invitation email | | `add_as_workspace_member` | bool | Also add the underlying user as a workspace team member | | `workspace_role` | string | Role if `add_as_workspace_member: true` | ### Example ```bash theme={null} curl -X POST https://api.scute.io/v1/app_l3Rx.../users \ -H "Authorization: Bearer scapp_..." \ -H "Content-Type: application/json" \ -d '{ "email": "jane@acme.com", "first_name": "Jane", "last_name": "Doe", "external_id": "acme-user-42", "user_meta": { "plan": "pro", "org": "acme" } }' ``` ### Response — `200 OK` ```json theme={null} { "user": { "id": "57f9b630-eb98-4ef4-...", "app_id": "app_l3Rx...", "workspace_id": "8a1c...", "user_id": "9d4e1f02-...", "msp_id": "msp_xxx", "external_id": "acme-user-42", "status": "active", "name": "Jane Doe", "email": "jane@acme.com", "phone": null, "email_verified": false, "phone_verified": false, "meta": { "plan": "pro", "org": "acme" }, "signup_date": "2026-05-25T10:00:00Z", "...": "..." }, "invite_sent": false, "workspace_membership_error": null } ``` `app_id` and `workspace_id` echo the parent — use them to map users back to the right client in your own database. *** ## Get a user ``` GET /v1/:app_id/users/:id ``` `:id` is the user UUID returned at creation. *** ## List / search users ``` GET /v1/:app_id/users?page=1&per_page=50&search=jane ``` ### Query parameters * `page`, `per_page` — pagination * `search` — fuzzy match on name / email / phone (requires `pg_trgm`) * `status` — filter by `active` or `inactive` ### Response ```json theme={null} { "users": [ /* ... */ ], "pagination": { "total": 1234, "page": 1, "per_page": 50, "total_pages": 25 } } ``` *** ## Update a user ``` PATCH /v1/:app_id/users/:id # partial update (only fields you send) PUT /v1/:app_id/users/:id # force-replace (use carefully) ``` Same body shape as create. `PATCH` leaves untouched fields alone; `PUT` overwrites the whole record. *** ## Activate / deactivate ``` POST /v1/:app_id/users/:id/activate POST /v1/:app_id/users/:id/deactivate ``` No body. Toggles `status` between `active` and `inactive`. Deactivated users can't sign in but aren't deleted. *** ## Delete a user ``` DELETE /v1/:app_id/users/:id ``` Hard-deletes the user, their sessions, credentials, and challenges within that app. Other apps in the workspace are untouched. *** ## Bulk: invite / import ``` POST /v1/:app_id/users/invite # send magic-link invites to a list POST /v1/:app_id/users/import # bulk create ``` Both accept arrays. See the individual endpoint references for body shapes. *** ## Errors | Status | When | | ------ | ---------------------------------------------------- | | `400` | Invalid identifier (malformed email / phone) | | `401` | Missing or invalid API key | | `403` | API key not authorized for this app | | `404` | App or user not found | | `422` | Validation error (e.g. `external_id` already in use) | # Show user Source: https://docs.scute.io/api-reference/users/show-user /swagger.json get /v1/{app_id}/users/{id} Get a specific user by ID # Update user metadata Source: https://docs.scute.io/api-reference/users/update-user-metadata /swagger.json patch /v1/{app_id}/users/{id} Update user metadata using the user ID from the path # Autocomplete search by identifier Source: https://docs.scute.io/api-reference/verification/autocomplete-search-by-identifier /swagger.json get /v1/verify/{app_id}/autocomplete Search for users by identifier (email or phone) within the current app # Create verification Source: https://docs.scute.io/api-reference/verification/create-verification /swagger.json post /v1/verify/{app_id}/verifications Create a verification (challenge) for the provided identifier # Create verification with intent Source: https://docs.scute.io/api-reference/verification/create-verification-with-intent /swagger.json post /v1/verify/{app_id}/verifications/intent Create a verification using intent metadata (Thread integration) # List verification requests Source: https://docs.scute.io/api-reference/verification/list-verification-requests /swagger.json get /v1/verify/{app_id}/verifications Get all verification requests for the current app # Show verification request Source: https://docs.scute.io/api-reference/verification/show-verification-request /swagger.json get /v1/verify/{app_id}/verifications/{id} Get details of a specific verification request # Verify code Source: https://docs.scute.io/api-reference/verification/verify-code /swagger.json post /v1/verify/{app_id}/verifications/{id}/verify Verify a verification request with the provided code or token # Finalize WebAuthn device registration Source: https://docs.scute.io/api-reference/webauthn/finalize-webauthn-device-registration /swagger.json post /v1/auth/{app_id}/devices/finalize Complete registration of a new WebAuthn device by verifying the credential response. Use the SDK instead of calling this manually. Requires a valid access token. # Finalize WebAuthn login Source: https://docs.scute.io/api-reference/webauthn/finalize-webauthn-login /swagger.json post /v1/auth/{app_id}/webauthn/login/finalize Finalize a WebAuthn authentication ceremony by verifying the credential assertion from the browser. Returns session tokens upon successful verification. Use the SDK instead of calling this manually. # Finalize WebAuthn registration Source: https://docs.scute.io/api-reference/webauthn/finalize-webauthn-registration /swagger.json post /v1/auth/{app_id}/webauthn/register/finalize Finalize a WebAuthn registration ceremony by verifying the credential response from the browser. Returns session tokens upon successful verification. Use the SDK instead of calling this manually. # Initialize WebAuthn login Source: https://docs.scute.io/api-reference/webauthn/initialize-webauthn-login /swagger.json post /v1/auth/{app_id}/webauthn/login/initialize Initialize a WebAuthn authentication ceremony. This endpoint returns the challenge options for the browser's WebAuthn API. Use the SDK instead of calling this manually. # Initialize WebAuthn registration Source: https://docs.scute.io/api-reference/webauthn/initialize-webauthn-registration /swagger.json post /v1/auth/{app_id}/webauthn/register/initialize Initialize a WebAuthn registration ceremony. This endpoint creates a new user (if they don't exist) and returns the challenge options for the browser's WebAuthn API. Use the SDK instead of calling this manually. # Register new WebAuthn device Source: https://docs.scute.io/api-reference/webauthn/register-new-webauthn-device /swagger.json post /v1/auth/{app_id}/devices/register Initiate registration of a new WebAuthn device (passkey) for the authenticated user. Returns credential creation options. Use the SDK instead of calling this manually. Requires a valid access token. # Revoke WebAuthn device Source: https://docs.scute.io/api-reference/webauthn/revoke-webauthn-device /swagger.json delete /v1/auth/{app_id}/devices/{id} Revoke a WebAuthn device (credential) by ID. Note: This endpoint is not used in the JS SDK. Use session revocation instead as sessions include device information. Requires a valid access token. # Create webhook endpoint Source: https://docs.scute.io/api-reference/webhooks/create-webhook-endpoint /swagger.json post /v1/apps/{app_id}/webhooks Create a new webhook endpoint for an application # Delete webhook endpoint Source: https://docs.scute.io/api-reference/webhooks/delete-webhook-endpoint /swagger.json delete /v1/apps/{app_id}/webhooks/{id} Delete a webhook endpoint # Get webhook deliveries Source: https://docs.scute.io/api-reference/webhooks/get-webhook-deliveries /swagger.json get /v1/apps/{app_id}/webhooks/{id}/deliveries Get delivery history for a webhook endpoint # Get webhook endpoint Source: https://docs.scute.io/api-reference/webhooks/get-webhook-endpoint /swagger.json get /v1/apps/{app_id}/webhooks/{id} Get details of a specific webhook endpoint # List webhook endpoints Source: https://docs.scute.io/api-reference/webhooks/list-webhook-endpoints /swagger.json get /v1/apps/{app_id}/webhooks Get all webhook endpoints for an application # Webhook Payload Source: https://docs.scute.io/api-reference/webhooks/payload Shape of the HTTP POST body Scute sends to your webhook endpoints. 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 ```http theme={null} POST Content-Type: application/json User-Agent: AppName-Webhooks/1.0 X-Webhook-Signature: t=,v1= ``` The signature is HMAC-SHA256 of `"."` using your endpoint's secret. Recompute it on your side and reject the request if it doesn't match. ## Body ```json theme={null} { "id": "", "verification_id": "", "challenge_id": "", "created_at": "2026-05-27T02:33:33Z", "event_type": "verification.success", "app_id": "app_l3RxEBGbQq5Q0b6T6K6N", "user": { "id": "", "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": "" } }, "data": { "purpose": "verify_contact", "method": "magic_link", "outcome": "verified", "intent": "Refund approval", "attempts": 0 }, "api_version": "v1" } ``` ### Top-level fields | Field | Type | Notes | | ----------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string (uuid) | The webhook delivery ID. Unique per delivery attempt — use it for idempotency keys. | | `verification_id` | string (uuid) | The challenge UUID that triggered the event. Same as `challenge_id`. | | `challenge_id` | string (uuid) | Alias for `verification_id`. Either works for lookups. | | `created_at` | ISO 8601 | When this delivery was created (not when the underlying event happened). | | `event_type` | string | The event slug. See below. | | `app_id` | string | Public app ID (`app_xxx`) the event belongs to. | | `user` | object \| omitted | The verifying user. Omitted for system events with no user. | | `metadata` | object \| omitted | Whatever you passed in `metadata` at challenge create time, plus any auto-attached info (e.g. `msp` for MSP challenges). Omitted when empty. | | `data` | object | Event-type-specific fields (purpose, method, outcome, etc.). | | `api_version` | string | Webhook endpoint API version. | ### `user` block | Field | Type | Notes | | ------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------- | | `id` | string (uuid) | The canonical user identifier for this app. | | `external_id` | string \| null | The ID you supplied at user creation, if any. Use this to map back to your own DB. | | `email` | string \| null | Primary email. | | `phone` | string \| null | Primary phone (E.164). | | `msp_id` | string \| null | Public 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: ```json theme={null} "msp": { "id": "msp_xxx", "msp_client_app_id": "app_clientxxx", "msp_client_workspace_id": "" } ``` 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 | Slug | When | | ------------------------------ | --------------------------------------------------------------------------------- | | `verification.attempted` | A challenge was created and delivery started. | | `verification.success` | User completed verification (correct code, magic link clicked, passkey approved). | | `verification.failed` | Wrong code / max attempts exceeded. | | `verification.denied` | User explicitly denied/rejected (consent mode). | | `verification.email.requested` | A new email verification was requested. | ### User | Slug | When | | --------------------- | ------------------------------------------------ | | `user.created` | New user account created via the management API. | | `user.updated` | User profile fields changed. | | `user.deleted` | User hard-deleted. | | `user.meta.updated` | `user_meta` fields changed. | | `user.status.changed` | Active ⇄ inactive. | | `user.invited` | Magic-link invite sent. | ### Auth / sessions | Slug | When | | ---------------------------------------------------------------------------- | --------------------------- | | `magiclink.login.init` | Magic link login initiated. | | `magiclink.auth.success` | Magic link authenticated. | | `otp.send.init` | OTP sent. | | `otp.verify.success` / `otp.verify.failed` | OTP verification outcome. | | `session.magic.created` / `session.otp.created` / `session.webauthn.created` | Session minted. | ### App / system | Slug | When | | --------------------------------------------- | ------------------------------------- | | `app.created` / `app.updated` / `app.deleted` | App lifecycle. | | `webhook.test` | Manual test fired from the dashboard. | ### Wildcard | Slug | When | | ---- | ------------------------ | | `*` | 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) ```js theme={null} 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](/api-reference/users/manage-users) for the challenge creation API. Use webhook endpoints unless you specifically need per-challenge routing. # Test webhook endpoint Source: https://docs.scute.io/api-reference/webhooks/test-webhook-endpoint /swagger.json post /v1/apps/{app_id}/webhooks/{id}/test Send a test webhook to verify endpoint configuration # Update webhook endpoint Source: https://docs.scute.io/api-reference/webhooks/update-webhook-endpoint /swagger.json patch /v1/apps/{app_id}/webhooks/{id} Update an existing webhook endpoint # List workspace apps Source: https://docs.scute.io/api-reference/workspace-apps/list-workspace-apps /swagger.json get /v1/workspaces/{workspace_id}/apps Get all apps for a specific workspace # Create workspace membership Source: https://docs.scute.io/api-reference/workspace-memberships/create-workspace-membership /swagger.json post /v1/workspaces/{workspace_id}/memberships Invite a member to the workspace. If user doesn't exist, they will be created. Requires admin role. # Delete workspace membership Source: https://docs.scute.io/api-reference/workspace-memberships/delete-workspace-membership /swagger.json delete /v1/workspaces/{workspace_id}/memberships/{id} Remove a member from the workspace. Requires admin role. Cannot remove yourself as owner. # List workspace memberships Source: https://docs.scute.io/api-reference/workspace-memberships/list-workspace-memberships /swagger.json get /v1/workspaces/{workspace_id}/memberships Get all members of a workspace # Update workspace membership Source: https://docs.scute.io/api-reference/workspace-memberships/update-workspace-membership /swagger.json patch /v1/workspaces/{workspace_id}/memberships/{id} Update a member's role in the workspace. Requires admin role. Cannot demote yourself as owner. # Create workspace session Source: https://docs.scute.io/api-reference/workspace-sessions/create-workspace-session /swagger.json post /v1/workspaces/{id}/sessions Create a user session from any app within a workspace using workspace API key # Create workspace Source: https://docs.scute.io/api-reference/workspaces/create-workspace /swagger.json post /v1/workspaces Create a new workspace. The authenticated user becomes the owner. # List workspaces Source: https://docs.scute.io/api-reference/workspaces/list-workspaces /swagger.json get /v1/workspaces Get all workspaces the authenticated user is a member of # Show workspace Source: https://docs.scute.io/api-reference/workspaces/show-workspace /swagger.json get /v1/workspaces/{id} Get workspace details by ID or slug # Update workspace Source: https://docs.scute.io/api-reference/workspaces/update-workspace /swagger.json patch /v1/workspaces/{id} Update workspace details. Requires member access. # April 2026 Source: https://docs.scute.io/changelogs/april-2026 Intent verification, risk scoring, entitlements, verification SDK, child workspaces # April 2026 *** ## Verification ### Verification modes * **Dismiss** — confirm, redirect. Default. * **Consent** — confirm, then approve/deny. ### Verification SDK `scute.verifications` on `@scute/js-core`: ```typescript theme={null} await scute.verifications.list({ status: "pending" }); await scute.verifications.approve(id); await scute.verifications.deny(id, "reason"); scute.verifications.onNew(callback); ``` ### Risk scoring Every verification includes `risk` in webhooks and API responses: ```json theme={null} { "score": 35, "level": "medium", "signals": ["new_ip"], "recommendation": "review" } ``` ## Dashboard * **Admin panel** — features, plans, usage, workspace detail with stats * **User risk score** — threat signals, impossible travel, suspicious IPs on Security tab * **Style page** — logo upload + color save fixed * **Templates** — General/All tabs, previews use app branding * **Sidebar** — active page highlight, app list dropdown * **Quick start** — inline API key generation *** ## SDK v0.6.0 * `@scute/js-core` — `scute.verifications` (list, approve, deny, verifyCode, onNew) * `@scute/nextjs-handlers` — updated * `@scute/react-hooks` — updated *** ## API * 12 webhook events with risk scoring, timing, device info * Phone normalization to E.164 # March 2026 Source: https://docs.scute.io/changelogs/march-2026 MFA everywhere, email OTP, fuzzy search # March 2026 *** ## API ### MFA now works across all login methods Previously, multi-factor authentication only kicked in through the challenge-based flow. Now it works everywhere, magic links, OTP, WebAuthn, all of it. If a user has MFA enrolled and your app requires it, they'll get an `mfa_required` response instead of tokens, regardless of how they signed in. WebAuthn logins skip MFA by default (it's already two factors, your device and your face), but you can turn that off per app if you're feeling strict. ### Email OTP The OTP endpoint used to be phone-only. Now it handles email too. Set `email_auth_type` to `"otp"` on your app and the same `/auth/:app_id/otps` endpoint accepts email addresses. It figures out what you sent it and does the right thing. ### User search got smarter The users endpoint now supports a `?q=` parameter. It does fuzzy matching across email, phone, and name using PostgreSQL's `pg_trgm` extension. Results come back ranked by relevance. No more exact-match-only filtering. ### Verifications API — new actions Two new endpoints: * `DELETE /v1/verify/:app_id/verifications/:id` — cancel a verification * `POST /v1/verify/:app_id/verifications/:id/resend` — resend it (cancels the old one, creates a fresh one with the same params) The create endpoint also now accepts full challenge params directly (`purpose`, `method`, `intent`, etc.) alongside the legacy format. ### MFA status endpoint New endpoint for the dashboard: ``` GET /v1/verify/:app_id/users/:user_id/mfa ``` Returns a user's MFA enrollments, which methods they have, backup codes remaining, everything you need to show on a user detail page. ### Rails 7.1 Upgraded from 7.0 to 7.1. Nothing breaks, everything's a little faster. *** ## Dashboard ### Auth settings redesign The authentication settings page got a full rework. You can now see a visual preview of your login flow, step by step, that updates as you change settings. New options in the settings panel: * **Email auth type** — switch between magic link and OTP * **MFA policy** — disabled, optional, or required * **Allowed MFA methods** — pick from TOTP, passkeys, email OTP, SMS OTP, backup codes * **WebAuthn skip MFA** — toggle whether passkey logins bypass MFA * **MFA grace period** — give users time to set up MFA after you enable it ### Challenges page "User Verification" in the nav is now "Challenges", because that's what they are. New full page for browsing, filtering, and creating challenges. You can filter by purpose, method, and status. Each challenge opens in a detail sheet with the full timeline and metadata. You can also create challenges directly from the dashboard now, with a dialog that supports all challenge types and purposes. ### MFA verify screen New MFA verification UI in the login flow. Shows a 6-digit code input (or 8-digit for backup codes), lets users switch between available MFA methods, and handles TOTP / email / SMS / backup codes. # Auth UI Source: https://docs.scute.io/guides/auth-ui Drop-in auth for React apps # Auth UI Add auth to your app in 2 minutes. Magic links, passkeys, OTP — all configured from your dashboard. ## Setup ```bash theme={null} npx create-scute-app ``` Or manually: ```bash theme={null} pnpm add @scute/js-core @scute/react-hooks @scute/auth-ui-react @scute/nextjs-handlers ``` ```bash theme={null} # .env.local NEXT_PUBLIC_SCUTE_APP_ID=your-app-id NEXT_PUBLIC_SCUTE_BASE_URL=https://api.scute.io SCUTE_SECRET=your-app-secret ``` *** ## Option 1: Drop-in component ```tsx theme={null} import { ScuteAuthGate } from "@scute/auth-ui-react"; ``` Shows login when not authenticated. Renders your app when authenticated. Handles passkey registration, magic links, OTP — everything. *** ## Option 2: Headless hook Full control over the UI. ```tsx theme={null} import { useScuteAuthFlow } from "@scute/auth-ui-react"; function Auth() { const auth = useScuteAuthFlow(); if (auth.isAuthenticated) return ; if (auth.view === "login") { return (
{ e.preventDefault(); auth.submitIdentifier(); }}> auth.setIdentifier(e.target.value)} placeholder="you@example.com" />
); } if (auth.view === "magic_pending") { return

Check your email: {auth.identifier}

; } if (auth.view === "webauthn_register") { return (

Register a passkey?

); } return

Loading...

; } ``` ### Views | View | What's happening | | --------------------------- | ----------------------------- | | `loading` | SDK initializing | | `login` | Show email input | | `magic_pending` | Magic link sent | | `magic_verifying` | Processing link from URL | | `otp_input` | Show code input | | `webauthn_verify` | Passkey prompt active | | `webauthn_register` | Offer passkey registration | | `webauthn_register_success` | Registered (auto-transitions) | | `error` | Show `auth.error` + retry | | `authenticated` | Done | ### Actions | Action | What it does | | --------------------------- | ---------------------------------- | | `auth.setIdentifier(email)` | Set email before submit | | `auth.submitIdentifier()` | Send magic link or trigger passkey | | `auth.submitOtp(code)` | Verify OTP code | | `auth.registerPasskey()` | Register passkey on this device | | `auth.skipPasskey()` | Skip registration, sign in | | `auth.retry()` | Back to login | | `auth.signOut()` | Sign out | *** ## Frameworks **Next.js** — `npx create-scute-app` handles everything. **Astro** — use as a React island with `client:only="react"`. **Remix / TanStack Start** — wrap with provider, use components. The CLI scaffolds the right files for each framework. *** ## Styling Default component uses `data-scute-*` attributes: ```css theme={null} [data-scute-view="login"] { } [data-scute-view="webauthn_register"] { } [data-scute-input] { } [data-scute-button="primary"] { } [data-scute-error] { } ``` Or use the headless hook and bring your own components. # Authentication Methods Source: https://docs.scute.io/guides/authentication-methods # Authentication Methods Scute provides a comprehensive set of authentication methods designed to handle various user scenarios while maintaining security and user experience. This guide explains the different authentication methods, their use cases, and how to implement them effectively. ## Authentication Flow Overview Scute's authentication system follows a hierarchical approach: 1. **WebAuthn/Passkeys** (highest security, best UX when available) 2. **Magic Links** (secure, no password required) 3. **OTP (One-Time Password)** (secure, works on all devices) 4. **OAuth/Social Login** (convenient for users with existing accounts) ## Core Authentication Methods ### `signInOrUp(identifier, options?)` **The recommended unified authentication method** that intelligently handles both sign-in and sign-up scenarios in a single call. This method provides the most seamless user experience by automatically selecting the best available authentication method. #### Authentication Flow: 1. **WebAuthn Check**: If supported and user has registered devices, attempts passkey authentication 2. **User Detection**: Checks if the identifier exists in the system 3. **Smart Routing**: * Existing user → Sign-in flow * New user → Sign-up flow 4. **Method Selection**: Based on identifier type and app configuration: * Email: Magic link (default) or OTP based on `email_auth_type` * Phone: Always OTP #### Use Cases: * Universal "Continue with Email/Phone" buttons * Simplified onboarding flows * Applications wanting to minimize user friction * Progressive web apps supporting multiple auth methods #### Examples: ```typescript theme={null} // Basic usage with email const { data, error } = await scuteClient.signInOrUp('user@example.com'); if (error) { console.error('Authentication failed:', error.message); return; } if (!data) { // User authenticated with passkey - they're signed in immediately console.log('User signed in with passkey'); navigate('/dashboard'); } else { // Additional verification needed (magic link or OTP sent) console.log('Verification sent via:', data.type); // 'magic_link' or 'otp' showVerificationUI(data.id); // Use data.id to check status } // Phone number authentication const { data, error } = await scuteClient.signInOrUp('+1234567890'); // With options to disable WebAuthn const { data, error } = await scuteClient.signInOrUp('user@example.com', { webauthn: 'disabled' }); ``` #### Response Handling: ```typescript theme={null} interface SignInOrUpResponse { data: { type: 'magic_link' | 'otp'; id: string; // Use this ID to check verification status } | null; error: ScuteError | null; } // Complete example with error handling const handleUniversalAuth = async (identifier: string) => { try { const { data, error } = await scuteClient.signInOrUp(identifier); if (error) { // Handle specific error types if (error instanceof TechnicalError) { showErrorMessage('Something went wrong. Please try again.'); } else { showErrorMessage(error.message); } return; } if (!data) { // WebAuthn success - user is already signed in onAuthenticationSuccess(); } else { // Show appropriate verification UI if (data.type === 'magic_link') { showMagicLinkSentMessage(identifier); } else { showOTPInputForm(identifier, data.id); } } } catch (err) { console.error('Unexpected error:', err); showErrorMessage('An unexpected error occurred.'); } }; ``` ### `signIn(identifier, options?)` **For existing users only.** This method will fail if the user doesn't exist in the system, making it ideal for dedicated sign-in flows where you want to ensure only existing users can authenticate. #### Key Features: * Requires user to exist in the system * Returns `IdentifierNotRecognizedError` if user doesn't exist * Still performs WebAuthn check first for registered users * Respects app configuration for fallback methods #### Use Cases: * Dedicated "Sign In" forms separate from registration * Applications with distinct sign-in/sign-up flows * When you want to validate user existence before authentication * Member-only areas or private applications #### Examples: ```typescript theme={null} // Basic sign-in const { data, error } = await scuteClient.signIn('user@example.com'); if (error) { if (error instanceof IdentifierNotRecognizedError) { showMessage('No account found with this email. Would you like to sign up?'); // Redirect to sign-up flow } else { showErrorMessage(error.message); } return; } // Handle successful authentication or verification needed handleAuthResponse(data); // Sign-in with WebAuthn disabled const { data, error } = await scuteClient.signIn('user@example.com', { webauthn: 'disabled' }); // Phone number sign-in const { data, error } = await scuteClient.signIn('+1234567890'); ``` #### WebAuthn Options: ```typescript theme={null} type ScuteWebauthnOption = 'strict' | 'optional' | 'disabled'; // Strict: Require WebAuthn if user has it enabled (fails if WebAuthn fails) await scuteClient.signIn('user@example.com', { webauthn: 'strict' }); // Optional: Try WebAuthn first, fallback to other methods (default) await scuteClient.signIn('user@example.com', { webauthn: 'optional' }); // Disabled: Skip WebAuthn entirely await scuteClient.signIn('user@example.com', { webauthn: 'disabled' }); ``` ### `signUp(identifier, options?)` **For new users only.** This method creates new user accounts and will fail if the user already exists, making it perfect for dedicated registration flows. #### Key Features: * Only creates new users * Returns `IdentifierAlreadyExistsError` if user exists * Accepts additional user metadata during registration * No WebAuthn check (since it's a new user) * Sends verification based on identifier type and app config #### Use Cases: * Dedicated registration/sign-up forms * Onboarding flows with user data collection * Invitation-based registration * Applications requiring explicit user consent for account creation #### Examples: ```typescript theme={null} // Basic sign-up const { data, error } = await scuteClient.signUp('newuser@example.com'); if (error) { if (error instanceof IdentifierAlreadyExistsError) { showMessage('An account with this email already exists. Sign in instead?'); // Redirect to sign-in flow } else { showErrorMessage(error.message); } return; } // Sign-up with user metadata const { data, error } = await scuteClient.signUp('newuser@example.com', { userMeta: { name: 'John Doe', company: 'Acme Inc', role: 'developer', agreed_to_terms: true, marketing_consent: false } }); // Phone number sign-up const { data, error } = await scuteClient.signUp('+1234567890', { userMeta: { name: 'Jane Smith', preferred_language: 'es' } }); ``` #### User Metadata Schema: ```typescript theme={null} interface UserMeta { // String fields name?: string; company?: string; role?: string; // Boolean fields agreed_to_terms?: boolean; marketing_consent?: boolean; newsletter_subscription?: boolean; // Number fields age?: number; // Any other custom fields defined in your app config [key: string]: string | number | boolean | undefined; } ``` #### Complete Registration Flow: ```typescript theme={null} const handleSignUp = async (identifier: string, userData: UserMeta) => { // Validate required fields first if (!userData.name || !userData.agreed_to_terms) { showErrorMessage('Please fill in all required fields'); return; } const { data, error } = await scuteClient.signUp(identifier, { userMeta: userData }); if (error) { if (error instanceof IdentifierAlreadyExistsError) { // Offer to sign in instead const shouldSignIn = await showConfirmDialog( 'An account already exists with this email. Sign in instead?' ); if (shouldSignIn) { return handleSignIn(identifier); } } else { showErrorMessage(error.message); } return; } // Show verification UI if (data.type === 'magic_link') { showMessage(`We've sent a verification link to ${identifier}`); } else { showOTPInput(identifier, data.id); } }; ``` ## Direct Authentication Methods ### `sendLoginMagicLink(identifier, webauthnEnabled?)` **Explicitly sends a magic link** for authentication, bypassing any passkey checks. This method gives you direct control over the authentication flow when you specifically want email-based authentication. #### Key Features: * Bypasses WebAuthn/passkey authentication * Works for both existing and new users * Sends email with secure, time-limited authentication link * Returns polling data to check verification status #### Use Cases: * "Continue with Email" buttons * Users who prefer email-based authentication * Fallback when WebAuthn fails or is unavailable * Shared device scenarios where passkeys aren't appropriate #### Examples: ```typescript theme={null} // Send magic link for existing or new user const { data, error } = await scuteClient.sendLoginMagicLink('user@example.com'); if (error) { showErrorMessage('Failed to send magic link'); return; } // Show user-friendly message showMessage('Check your email for a sign-in link'); // Optional: Poll for verification status pollForMagicLinkVerification(data.id); // Disable WebAuthn in the magic link token const { data, error } = await scuteClient.sendLoginMagicLink( 'user@example.com', false // webauthnEnabled = false ); ``` #### Magic Link Verification: ```typescript theme={null} // Poll for magic link verification const pollForMagicLinkVerification = async (magicLinkId: string) => { const maxAttempts = 30; // 5 minutes with 10-second intervals let attempts = 0; const checkStatus = async () => { try { const { data, error } = await scuteClient.getMagicLinkStatus(magicLinkId); if (!error && data) { // Magic link was clicked - sign in the user await scuteClient.signInWithTokenPayload(data); onAuthenticationSuccess(); return; } } catch (err) { // Continue polling } attempts++; if (attempts < maxAttempts) { setTimeout(checkStatus, 10000); // Check every 10 seconds } else { showMessage('Magic link expired. Please try again.'); } }; checkStatus(); }; // Handle magic link from URL (when user clicks the link) const handleMagicLinkFromURL = async () => { const token = scuteClient.getMagicLinkToken(); // Gets token from current URL if (token) { const { error } = await scuteClient.signInWithMagicLinkToken(token); if (error) { showErrorMessage('Invalid or expired magic link'); } else { onAuthenticationSuccess(); } } }; ``` ### `sendLoginOtp(identifier, webauthnEnabled?)` **Explicitly sends a one-time password (OTP)** for authentication. This method is ideal for phone-based authentication or when you need a more immediate verification method. #### Key Features: * Bypasses WebAuthn/passkey authentication * Sends SMS or email-based OTP * Works for both existing and new users * 6-digit numeric code with time expiration #### Use Cases: * Phone number authentication * Regions where SMS is preferred over email * Two-factor authentication flows * Users without reliable email access * High-security applications #### Examples: ```typescript theme={null} // Send OTP to phone number const { data, error } = await scuteClient.sendLoginOtp('+1234567890'); if (error) { showErrorMessage('Failed to send verification code'); return; } showMessage('Enter the 6-digit code sent to your phone'); showOTPInput(data.id); // Send OTP to email (if app is configured for email OTP) const { data, error } = await scuteClient.sendLoginOtp('user@example.com'); ``` #### OTP Verification: ```typescript theme={null} // Complete OTP verification flow const handleOTPVerification = async (otp: string, identifier: string) => { if (!/^\d{6}$/.test(otp)) { showErrorMessage('Please enter a valid 6-digit code'); return; } try { const { data, error } = await scuteClient.verifyOtp(otp, identifier); if (error) { showErrorMessage('Invalid or expired code. Please try again.'); return; } // OTP verified - sign in the user await scuteClient.signInWithTokenPayload(data.authPayload); onAuthenticationSuccess(); } catch (err) { showErrorMessage('Verification failed. Please try again.'); } }; // Resend OTP functionality const resendOTP = async (identifier: string) => { const { data, error } = await scuteClient.sendLoginOtp(identifier); if (!error) { showMessage('New verification code sent'); return data.id; // New OTP session ID } else { showErrorMessage('Failed to resend code'); return null; } }; ``` ### `signInWithOAuthProvider(provider)` **Initiates OAuth authentication** with external providers like Google, GitHub, or other configured OAuth services. #### Key Features: * Redirects to OAuth provider for authentication * Handles federated identity management * Supports multiple OAuth providers * Automatic account linking for existing users #### Use Cases: * Social login (Google, Facebook, GitHub, etc.) * Enterprise SSO integration * Reducing password fatigue * Accessing provider-specific APIs * Streamlined onboarding for users with existing accounts #### Examples: ```typescript theme={null} // Redirect to Google OAuth await scuteClient.signInWithOAuthProvider('google'); // Redirect to GitHub OAuth await scuteClient.signInWithOAuthProvider('github'); // Get OAuth URL without redirecting (useful for custom handling) const googleUrl = scuteClient.getOAuthUrl('google'); window.open(googleUrl, '_blank'); // Open in new tab // Check available OAuth providers const { data: appData } = await scuteClient.getAppData(); const providers = appData.oauth_providers || []; providers.forEach(provider => { console.log(`Available: ${provider.name} (${provider.provider})`); }); ``` #### OAuth Configuration Example: ```typescript theme={null} // Display OAuth providers dynamically const renderOAuthButtons = (providers: ScuteOAuthProviderConfig[]) => { return providers.map(provider => ( )); }; // Handle OAuth callback (in your callback page) const handleOAuthCallback = async () => { try { const { error } = await scuteClient.signInWithMagicLink(); if (error) { showErrorMessage('OAuth authentication failed'); navigate('/login'); } else { navigate('/dashboard'); } } catch (err) { console.error('OAuth callback error:', err); navigate('/login'); } }; ``` ## Token-Based Authentication Methods ### `signInWithTokenPayload(payload)` **Internal method** used after successful verification of magic links, OTP, or OAuth to establish the user session. This method completes the authentication flow by setting up the user's session. #### Key Features: * Establishes authenticated session * Triggers authentication state change events * Handles user data fetching * Sets up session persistence * Remembers user identifier for future logins #### Use Cases: * Completing magic link verification * Completing OTP verification * Handling OAuth callbacks * Custom authentication flows * Testing and development scenarios #### Examples: ```typescript theme={null} // This method is typically called internally, but can be used for custom flows const completeAuthentication = async (tokenPayload: ScuteTokenPayload) => { const { error } = await scuteClient.signInWithTokenPayload(tokenPayload); if (error) { showErrorMessage('Authentication failed'); } else { // User is now signed in navigate('/dashboard'); } }; // Common usage patterns (these methods call signInWithTokenPayload internally): // 1. Magic link verification const { data, error } = await scuteClient.verifyMagicLinkToken(token); if (!error) { await scuteClient.signInWithTokenPayload(data.authPayload); } // 2. OTP verification const { data, error } = await scuteClient.verifyOtp(otp, identifier); if (!error) { await scuteClient.signInWithTokenPayload(data.authPayload); } // 3. Magic link from URL const { error } = await scuteClient.signInWithMagicLink(); // Handles token extraction and signInWithTokenPayload ``` #### Token Payload Structure: ```typescript theme={null} interface ScuteTokenPayload { access: string; // JWT access token access_expires_at: string; // ISO timestamp refresh?: string | null; // Refresh token (if enabled) refresh_expires_at?: string | null; // ISO timestamp } ``` ### `signOut()` **Securely ends the user session** and cleans up all authentication state. This method ensures complete logout across all stored data. #### Key Features: * Revokes refresh token on server * Clears local session storage * Removes stored credentials * Triggers sign-out event for UI updates * Works across browser tabs (broadcast channel) #### Use Cases: * User-initiated logout * Session timeout handling * Security-sensitive operations * Switching between accounts * Admin-initiated session termination #### Examples: ```typescript theme={null} // Basic sign out const handleSignOut = async () => { const success = await scuteClient.signOut(); if (success) { showMessage('Signed out successfully'); navigate('/login'); } else { // Still clears local state even if server call fails showMessage('Signed out (offline)'); navigate('/login'); } }; // Sign out with confirmation const handleSignOutWithConfirmation = async () => { const confirmed = await showConfirmDialog( 'Are you sure you want to sign out?' ); if (confirmed) { await handleSignOut(); } }; // Listen for sign-out events (useful for global state management) scuteClient.onAuthStateChange((event, session, user) => { if (event === 'signed_out') { // Clear application state clearUserData(); clearLocalCache(); navigate('/login'); } }); // Automatic sign out on session expiration scuteClient.onAuthStateChange((event, session, user) => { if (event === 'session_expired') { showMessage('Your session has expired. Please sign in again.'); navigate('/login'); } }); ``` #### Sign Out Return Value: ```typescript theme={null} // signOut() returns a boolean indicating server-side success const success = await scuteClient.signOut(); if (success) { // Refresh token was successfully revoked on server } else { // Local state was cleared, but server revocation failed // This can happen due to network issues or already-expired tokens // User is still effectively signed out locally } ``` ## WebAuthn/Passkey Methods ### `addDevice()` **Registers a new WebAuthn device/passkey** for the currently authenticated user. This enhances security and provides passwordless authentication for future logins. #### Key Features: * Requires active authentication session * Triggers browser's WebAuthn UI * Supports various authenticator types (platform, roaming) * Stores credential for future authentication * Emits registration events for UI feedback #### Examples: ```typescript theme={null} // Add device after user signs in const handleAddPasskey = async () => { try { const { data, error } = await scuteClient.addDevice(); if (error) { if (error.message.includes('cancelled')) { showMessage('Passkey setup was cancelled'); } else { showErrorMessage('Failed to set up passkey'); } return; } showMessage('Passkey added successfully! You can now sign in faster.'); } catch (err) { showErrorMessage('Passkey not supported on this device'); } }; // Check if WebAuthn is supported before showing the option const shouldShowPasskeyOption = () => { return scuteClient.isWebauthnSupported(); }; // Listen for passkey registration events scuteClient.onAuthStateChange((event, session, user) => { switch (event) { case 'webauthn_register_start': showMessage('Setting up your passkey...'); break; case 'webauthn_register_success': showMessage('Passkey set up successfully!'); break; } }); ``` ### `isWebauthnSupported()` **Checks if WebAuthn is supported** on the current device and browser. ```typescript theme={null} const isSupported = scuteClient.isWebauthnSupported(); if (isSupported) { // Show passkey options in UI } else { // Hide passkey features } ``` ### `isAnyDeviceRegistered()` **Checks if the current user has any registered WebAuthn devices** on this browser/device. ```typescript theme={null} // Requires authentication try { const hasDevices = await scuteClient.isAnyDeviceRegistered(); if (hasDevices) { showMessage('You can sign in with your passkey'); } else { showPasskeySetupPrompt(); } } catch (error) { // User not authenticated } ``` ## Authentication State Management ### `onAuthStateChange(callback)` **Listen to authentication state changes** to keep your UI synchronized with the user's authentication status. This is the primary method for handling authentication events in your application. #### Authentication Events: * `signed_in` - User successfully authenticated * `signed_out` - User signed out or session ended * `initial_session` - Initial session check completed * `session_refetch` - Session data refreshed * `session_expired` - Session expired and needs renewal * `token_refreshed` - Access token automatically refreshed * `magic_pending` - Magic link or OTP sent, waiting for verification * `magic_new_device_pending` - Magic link sent due to new device detection * `otp_pending` - OTP sent, waiting for verification * `otp_new_device_pending` - OTP sent due to new device detection * `webauthn_register_start` - WebAuthn registration started * `webauthn_register_success` - WebAuthn registration completed * `webauthn_verify_start` - WebAuthn verification started * `webauthn_verify_success` - WebAuthn verification completed #### Examples: ```typescript theme={null} // Basic authentication state handling const unsubscribe = scuteClient.onAuthStateChange((event, session, user) => { console.log('Auth event:', event); switch (event) { case 'signed_in': console.log('User signed in:', user?.email); navigate('/dashboard'); break; case 'signed_out': console.log('User signed out'); navigate('/login'); break; case 'session_expired': showMessage('Your session has expired. Please sign in again.'); navigate('/login'); break; case 'magic_pending': showMessage('Check your email for a sign-in link'); break; case 'otp_pending': showOTPInput(); break; case 'webauthn_register_start': showMessage('Setting up your passkey...'); break; } }); // Clean up listener when component unmounts return () => unsubscribe(); ``` #### Advanced State Management: ```typescript theme={null} // React hook for authentication state const useAuth = () => { const [session, setSession] = useState(null); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const unsubscribe = scuteClient.onAuthStateChange((event, session, user) => { setSession(session); setUser(user); if (event === 'initial_session') { setLoading(false); } }); return unsubscribe; }, []); return { session, user, loading }; }; // Global state management (Redux/Zustand example) const authSlice = createSlice({ name: 'auth', initialState: { user: null, session: null, loading: true }, reducers: { setAuth: (state, action) => { state.user = action.payload.user; state.session = action.payload.session; state.loading = false; }, clearAuth: (state) => { state.user = null; state.session = null; state.loading = false; } } }); // Set up listener scuteClient.onAuthStateChange((event, session, user) => { if (event === 'signed_in' || event === 'initial_session') { store.dispatch(authSlice.actions.setAuth({ user, session })); } else if (event === 'signed_out') { store.dispatch(authSlice.actions.clearAuth()); } }); ``` ### `getUser(accessToken?)` **Get the current authenticated user's data** or verify a specific access token. #### Examples: ```typescript theme={null} // Get current user (uses stored session) const { data, error } = await scuteClient.getUser(); if (error) { console.log('User not authenticated'); } else { console.log('Current user:', data.user); } // Verify specific access token const { data, error } = await scuteClient.getUser(accessToken); ``` ### `getAuthToken()` **Get the current access token** from the stored session. ```typescript theme={null} const { data, error } = await scuteClient.getAuthToken(); if (!error) { console.log('Access token:', data.access); console.log('Expires at:', data.access_expires_at); } ``` ## Utility Methods ### `identifierExists(identifier)` **Check if a user exists** with the given email or phone number without initiating authentication. ```typescript theme={null} try { const user = await scuteClient.identifierExists('user@example.com'); if (user) { console.log('User exists:', user.email); console.log('Has passkey:', user.webauthn_enabled); } else { console.log('No user found with this identifier'); } } catch (error) { console.error('Failed to check identifier:', error); } ``` ### `getAppData(fresh?)` **Get application configuration and settings** to customize the authentication flow. ```typescript theme={null} const { data, error } = await scuteClient.getAppData(); if (!error) { console.log('App name:', data.name); console.log('Email auth type:', data.email_auth_type); // 'magic' or 'otp' console.log('OAuth providers:', data.oauth_providers); console.log('Required identifiers:', data.required_identifiers); console.log('User metadata schema:', data.user_meta_data_schema); } // Force fresh data from server const { data } = await scuteClient.getAppData(true); ``` ## Session Management ### `listUserSessions()` **Get all active sessions** for the current user across all devices. ```typescript theme={null} const { data, error } = await scuteClient.listUserSessions(); if (!error) { data.sessions.forEach(session => { console.log('Session:', session.display_name); console.log('Type:', session.type); // 'webauthn', 'magic', 'oauth', etc. console.log('Last used:', session.last_used_at); console.log('Platform:', session.platform); console.log('Browser:', session.browser); }); } ``` ### `revokeSession(sessionId, credentialId?)` **Revoke a specific session** from another device. ```typescript theme={null} // Revoke a specific session await scuteClient.revokeSession('session-id'); // Revoke session and associated WebAuthn credential await scuteClient.revokeSession('session-id', 'credential-id'); ``` ### `removeDeviceCredential(credentialId)` **Remove a WebAuthn credential** without affecting the session. ```typescript theme={null} await scuteClient.removeDeviceCredential('credential-id'); ``` ## Error Handling Scute provides specific error types for different authentication scenarios. For comprehensive error handling documentation, see the [Error Handling Guide](/docs/guides/error-handling). ```typescript theme={null} // Basic error handling const { data, error } = await scuteClient.signInOrUp(identifier); if (error) { console.error('Authentication error:', error.message); // Handle error based on type return; } // Continue with success flow ``` ## Best Practices ### 1. **Start with `signInOrUp` for Maximum Flexibility** ```typescript theme={null} // Recommended: Universal authentication const handleAuth = async (identifier: string) => { const { data, error } = await scuteClient.signInOrUp(identifier); if (error) { handleAuthError(error); return; } // Handle success... }; ``` ### 2. **Implement Progressive Enhancement** ```typescript theme={null} // Check capabilities and adjust UI accordingly const AuthComponent = () => { const [webauthnSupported] = useState(scuteClient.isWebauthnSupported()); const [appData, setAppData] = useState(null); useEffect(() => { scuteClient.getAppData().then(({ data }) => setAppData(data)); }, []); return (
{/* Always show primary authentication */} {/* Show OAuth if configured */} {appData?.oauth_providers?.map(provider => ( ))} {/* Show passkey option if supported */} {webauthnSupported && ( )}
); }; ``` ### 3. **Handle Network Connectivity** ```typescript theme={null} const handleAuthWithRetry = async (identifier: string, maxRetries = 3) => { let attempts = 0; while (attempts < maxRetries) { try { const result = await scuteClient.signInOrUp(identifier); return result; } catch (error) { attempts++; if (error.message.includes('network') && attempts < maxRetries) { await new Promise(resolve => setTimeout(resolve, 1000 * attempts)); continue; } throw error; } } }; ``` ### 4. **Implement Proper Loading States** ```typescript theme={null} const AuthForm = () => { const [loading, setLoading] = useState(false); const [step, setStep] = useState('input'); // 'input', 'verification', 'success' const handleSubmit = async (identifier: string) => { setLoading(true); try { const { data, error } = await scuteClient.signInOrUp(identifier); if (error) { handleAuthError(error); return; } if (!data) { // WebAuthn success setStep('success'); } else { // Need verification setStep('verification'); } } finally { setLoading(false); } }; return (
{step === 'input' && (
)} {step === 'verification' && ( )} {step === 'success' && (
Welcome back!
)}
); }; ``` ### 5. **Security Considerations** * **Always use HTTPS in production** to protect tokens and user data * **Validate user input** before passing to authentication methods * **Implement session timeout handling** using auth state events * **Use appropriate WebAuthn settings** based on your security requirements ### 6. **Testing Authentication Flows** ```typescript theme={null} // Example: Test helper for authentication flows const createTestAuthClient = (config = {}) => { return createClient({ appId: 'test-app-id', baseUrl: 'http://localhost:3000', debug: true, ...config }); }; // Example: Mock authentication for testing const mockSuccessfulAuth = () => { jest.spyOn(scuteClient, 'signInOrUp').mockResolvedValue({ data: null, error: null }); }; ``` ## Complete Implementation Example Here's a comprehensive example showing how to implement a full authentication flow with all the best practices: ```typescript theme={null} import { useState, useEffect } from 'react'; import { createClient, type ScuteError } from '@scute/auth'; const scuteClient = createClient({ appId: 'your-app-id', baseUrl: 'https://api.scute.io' }); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const unsubscribe = scuteClient.onAuthStateChange((event, session, user) => { setUser(user); if (event === 'initial_session') { setLoading(false); } }); return unsubscribe; }, []); return ( {children} ); }; export const LoginForm = () => { const [identifier, setIdentifier] = useState(''); const [step, setStep] = useState('input'); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const handleAuth = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(''); const validationError = validateIdentifier(identifier); if (validationError) { setError(validationError); setLoading(false); return; } try { const { data, error } = await scuteClient.signInOrUp(identifier); if (error) { setError(getErrorMessage(error)); } else if (!data) { // WebAuthn success - user is signed in setStep('success'); } else { // Show verification step setStep('verification'); } } catch (err) { setError('An unexpected error occurred'); } finally { setLoading(false); } }; return (
{step === 'input' && (

Welcome

setIdentifier(e.target.value)} disabled={loading} /> {error &&
{error}
}
)} {step === 'verification' && ( )} {step === 'success' && (
Welcome! Redirecting...
)}
); }; ``` This comprehensive guide covers all the authentication methods available in Scute, providing you with the knowledge and examples needed to implement secure, user-friendly authentication in your application. # Child Workspaces Source: https://docs.scute.io/guides/child-workspaces Multi-tenant verification with isolated branding and credentials # Child Workspaces Provision isolated verification environments under your workspace. Each child gets its own app, API key, branding, and webhooks. Auth: **parent workspace API key** in `Authorization: Bearer {key}`. *** ## Create ``` POST /v1/workspaces/{workspace_id}/children ``` ```bash theme={null} curl -X POST https://api.scute.io/v1/workspaces/{workspace_id}/children \ -H "Authorization: Bearer {api_key}" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Corp", "branding_display_name": "Acme Corp", "branding_color": "#2563EB", "branding_logo_url": "https://acme.com/logo.png", "countries": ["us", "ca"], "magic_link_expiry": 900, "webhook_url": "https://your-server.com/webhooks/scute" }' ``` | Parameter | Type | Description | | ------------------------------------ | --------- | -------------------------------------------- | | `name` | string | Workspace name | | `branding_display_name` | string | Shown on verification pages | | `branding_color` | string | Hex color for buttons/accents | | `branding_logo_url` | string | Logo on verification pages | | `magic_link_expiry` | int | Seconds. Default: 900 | | `countries` | string\[] | Country codes | | `webhook_url` | string | Receives all verification events | | `verification_mode` | string | `dismiss` (default) or `consent` | | `verification_redirect_url_template` | string | Supports `{ticket_id}`, `{contact_id}`, etc. | | `verification_redirect_delay` | int | Seconds before redirect. Default: 4 | | `verification_success_message` | string | Custom success text | Response includes `app_id`, `api_key_token`, and `api_key_id`. Save the key — it's only returned once. Scute auto-provisions: verify-only app, SMS provider, API key, and webhook endpoint (subscribed to all `verification.*` events). *** ## List ```bash theme={null} curl https://api.scute.io/v1/workspaces/{workspace_id}/children \ -H "Authorization: Bearer {api_key}" ``` Returns `{ workspace_id, children: [...] }`. *** ## Get ```bash theme={null} curl https://api.scute.io/v1/workspaces/{workspace_id}/children/{child_id} \ -H "Authorization: Bearer {api_key}" ``` Includes `api_key_token` and `webhook_endpoints` with secrets. *** ## Update ``` PATCH /v1/workspaces/{workspace_id}/children/{child_id} ``` Send only the fields you want to change. Branding syncs to the child's app automatically. ```bash theme={null} curl -X PATCH https://api.scute.io/v1/workspaces/{workspace_id}/children/{child_id} \ -H "Authorization: Bearer {api_key}" \ -H "Content-Type: application/json" \ -d '{ "branding_color": "#DC2626", "magic_link_expiry": 1800 }' ``` *** ## Delete ```bash theme={null} curl -X DELETE https://api.scute.io/v1/workspaces/{workspace_id}/children/{child_id} \ -H "Authorization: Bearer {api_key}" ``` Returns `204 No Content`. Soft-delete. *** ## Full onboarding flow ```bash theme={null} # 1. Create child RESPONSE=$(curl -s -X POST https://api.scute.io/v1/workspaces/{workspace_id}/children \ -H "Authorization: Bearer {parent_api_key}" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Corp", "branding_display_name": "Acme Corp", "branding_color": "#2563EB", "webhook_url": "https://your-server.com/webhooks/acme" }') CHILD_APP_ID=$(echo $RESPONSE | jq -r '.child_workspace.app_id') CHILD_API_KEY=$(echo $RESPONSE | jq -r '.child_workspace.api_key_token') # 2. Get M2M token M2M_TOKEN=$(curl -s -X POST https://api.scute.io/v1/apps/${CHILD_APP_ID}/m2m/token \ -H "Authorization: Bearer ${CHILD_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"client_name": "backend"}' | jq -r '.short_token') # 3. Send verification curl -X POST https://api.scute.io/v1/verify/${CHILD_APP_ID}/verifications/intent \ -H "X-Authorization: Bearer ${M2M_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "intent_name": "Password Reset", "method": "sms", "verification_type": "magic_link", "meta_data": { "contact_email": "user@acme.com", "contact_phones": [{"phone_number": "+14155551234", "phone_type": "mobile"}], "ticket_id": "TICKET-001" } }' ``` The verification page shows the child's branding, not yours. *** ## Errors | Status | Meaning | | ------ | ------------------------------------ | | 401 | Bad or missing API key | | 403 | Key doesn't belong to this workspace | | 404 | Workspace or child not found | | 422 | Invalid params | # Data migration through API Source: https://docs.scute.io/guides/data_migration_with_api # Migrating Your User Data to Scute Welcome to our user data migration guide! This document will walk you through the process of moving your user data into Scute's system. We'll cover everything you need to know, from understanding how we store user information to practical examples of using our API. ## Understanding User Data in Scute Before we dive into the technical details, let's understand how Scute organizes user information. Each user in our system has two main components: ### Basic User Information Every user has these fundamental attributes: * A unique identifier (UUID) * Contact information (email and/or phone) * Name * Account status (active, pending, inactive, or imported) * Verification timestamps for email and phone ### Custom User Attributes Need to store additional information about your users? We've got you covered! You can add custom fields with various data types: 📝 **Available Data Types:** * `string` - For general text data * `boolean` - For yes/no flags * `integer` - For whole numbers * `date` - For timestamps and calendar dates * `phone` - For phone numbers with validation * `email` - For email addresses with validation * `text` - For longer text content * `url` - For web links ## Moving Your Users to Scute ### The API Endpoint Here's how you can create new users through our API: ```http title="API Request & Response" theme={null} POST /v1/auth/:app_id/users Request Body: { "identifier": "", // Required: User's email or phone "user_meta": { // Optional: Custom user metadata "field1": "value1", "field2": "value2" } } Responses: 200 OK { "user": { "id": "uuid", "email": "user@example.com", "phone": "+1234567890", "meta": { ... } }, "user_meta_errors": { } // Any errors in metadata validation } 400 Bad Request { "error": "Identifier invalid", "error_code": "invalid_identifier" } ``` ### Example Usage #### CURL Example ```bash title="Create User with CURL" theme={null} # Create a new user with email curl -X POST 'https://api.scute.io/v1/auth/your_app_id/users' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "identifier": "user@example.com", "user_meta": { "company": "Acme Inc", "role": "admin", "signup_date": "2024-03-20" } }' # Create a user with phone number curl -X POST 'https://api.scute.io/v1/auth/your_app_id/users' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "identifier": "+14155552671", "user_meta": { "location": "San Francisco", "is_verified": true } }' ``` #### Shell Script Example ```bash title="Migration Script" theme={null} #!/bin/bash # Configuration API_KEY="your_api_key_here" APP_ID="your_app_id" API_URL="https://api.scute.io/v1/auth/$APP_ID/users" # Function to create a user create_user() { local identifier=$1 local meta=$2 response=$(curl -s -X POST "$API_URL" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"identifier\": \"$identifier\", \"user_meta\": $meta }") echo "$response" } # Example usage user_meta='{ "company": "Tech Corp", "department": "Engineering", "start_date": "2024-03-20" }' # Create user with email create_user "engineer@techcorp.com" "$user_meta" # Create user with phone create_user "+14155552671" "$user_meta" ``` ## Important Things to Keep in Mind ### Data Validation * Email addresses must match standard format * Phone numbers should be in international format * Custom fields must match their declared types ### Security Best Practices * Always use HTTPS for API calls * Keep your API keys secure * Handle personal information with care ### Tips for a Smooth Migration 1. **Back Up Your Data**: Always keep a backup of your source data before starting 2. **Start Small**: Test with a small batch of users first 3. **Verify as You Go**: Check each batch after migration 4. **Monitor Progress**: Keep track of successful and failed migrations Need help with your migration? Don't hesitate to reach out to our support team! # Error Handling Source: https://docs.scute.io/guides/error-handling # Error Handling Proper error handling is crucial for creating robust authentication flows that provide clear feedback to users while maintaining security. Scute provides comprehensive error handling utilities to help you manage various failure scenarios gracefully. ## Overview Scute's error handling system is designed to: * **Provide meaningful error messages** that help users understand what went wrong * **Maintain security** by not exposing sensitive information in error messages * **Enable proper logging** for debugging and monitoring * **Support different error types** for various authentication scenarios * **Offer retry mechanisms** for transient failures ## Error Types Scute categorizes errors into several types to help you handle them appropriately: ### Custom Scute Errors * **identifier-not-recognized**: The provided identifier (email/username) is not recognized in the system * **identifier-already-exists**: A user with this identifier already exists during registration * **identifier-invalid**: The identifier format is invalid (e.g., malformed email) * **new-device**: Authentication attempt from a new/unrecognized device * **login-required**: User must be logged in to access the resource * **invalid-auth-token**: The authentication token is invalid or expired * **unknown-sign-in**: An unknown error occurred during sign-in * **invalid-magic-link**: The magic link is invalid, expired, or already used ### HTTP Errors * **4xx Client Errors**: Bad request, unauthorized, forbidden, not found errors * **5xx Server Errors**: Internal server errors, service unavailable * **502, 503, 504**: Network-related errors (Bad Gateway, Service Unavailable, Gateway Timeout) ### WebAuthn Errors * **ERROR\_CEREMONY\_ABORTED**: The WebAuthn ceremony was aborted by the user * **ERROR\_INVALID\_DOMAIN**: The current domain is invalid for WebAuthn * **ERROR\_INVALID\_RP\_ID**: The Relying Party ID is invalid for this domain * **ERROR\_INVALID\_USER\_ID\_LENGTH**: User ID must be between 1 and 64 characters * **ERROR\_MALFORMED\_PUBKEYCREDPARAMS**: Public key credential parameters are malformed * **ERROR\_AUTHENTICATOR\_GENERAL\_ERROR**: General authenticator error * **ERROR\_AUTHENTICATOR\_MISSING\_DISCOVERABLE\_CREDENTIAL\_SUPPORT**: Authenticator doesn't support discoverable credentials * **ERROR\_AUTHENTICATOR\_MISSING\_USER\_VERIFICATION\_SUPPORT**: Authenticator doesn't support user verification * **ERROR\_AUTHENTICATOR\_PREVIOUSLY\_REGISTERED**: The authenticator was previously registered * **ERROR\_AUTHENTICATOR\_NO\_SUPPORTED\_PUBKEYCREDPARAMS\_ALG**: No supported algorithms in pubKeyCredParams * **ERROR\_PASSTHROUGH\_SEE\_CAUSE\_PROPERTY**: Error passed through from platform (check cause property) ### Technical Errors * **TechnicalError**: Internal technical errors that should be handled gracefully ## Basic Error Handling Here's how to implement basic error handling with Scute: ```typescript theme={null} import { getMeaningfulError, ScuteError } from "@scute/js-core"; const handleAuthError = (error: any) => { const errorResult = getMeaningfulError(error); // Log for debugging (include full error details) console.error("Authentication error:", { message: errorResult.message, isFatal: errorResult.isFatal, code: error.code, timestamp: new Date().toISOString(), stack: error.stack }); // Show user-friendly message setErrorMessage( errorResult.message || "Authentication failed. Please try again." ); // Handle specific error types switch (error.code) { case "identifier-not-recognized": setErrorMessage("This email address is not recognized. Please check your email or sign up for a new account."); // Show sign up option setShowSignUpOption(true); break; case "identifier-already-exists": setErrorMessage("An account with this email already exists. Please sign in instead."); // Redirect to sign in setShowSignInForm(true); break; case "identifier-invalid": setErrorMessage("Please enter a valid email address."); // Focus on email field focusEmailField(); break; case "new-device": setErrorMessage("New device detected. Please check your email for verification."); // Show device verification flow setShowDeviceVerification(true); break; case "login-required": setErrorMessage("Please sign in to continue."); // Redirect to login redirectToLogin(); break; case "invalid-auth-token": setErrorMessage("Your session has expired. Please sign in again."); // Clear stored tokens and redirect clearTokensAndRedirect(); break; case "invalid-magic-link": setErrorMessage("This magic link is invalid or has expired. Please request a new one."); // Show magic link request form setShowMagicLinkForm(true); break; case "ERROR_CEREMONY_ABORTED": setErrorMessage("Authentication was cancelled. Please try again."); // Re-enable authentication buttons setIsAuthDisabled(false); break; case "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED": setErrorMessage("This authenticator is already registered. Please use a different one or sign in."); break; case 502: case 503: case 504: setErrorMessage("Service is temporarily unavailable. Please try again in a few moments."); // Enable retry mechanism setShowRetryButton(true); break; default: // Handle fatal vs non-fatal errors if (errorResult.isFatal) { setErrorMessage("A technical error occurred. Please refresh the page and try again."); setShowRefreshButton(true); } else { setErrorMessage(errorResult.message || "An unexpected error occurred. Please try again."); } } }; ``` ## Advanced Error Handling Patterns ### Error Boundary for React Applications Implement an error boundary to catch and handle authentication errors at the component level: ```typescript theme={null} import React from 'react'; import { getMeaningfulError } from "@scute/js-core"; interface ErrorBoundaryState { hasError: boolean; error?: Error; } class AuthErrorBoundary extends React.Component< React.PropsWithChildren<{}>, ErrorBoundaryState > { constructor(props: React.PropsWithChildren<{}>) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { // Log error details for debugging console.error('Auth Error Boundary caught an error:', { error: getMeaningfulError(error), errorInfo, timestamp: new Date().toISOString() }); // Send error to monitoring service if (process.env.NODE_ENV === 'production') { sendErrorToMonitoring(error, errorInfo); } } render() { if (this.state.hasError) { return (

Authentication Error

{getMeaningfulError(this.state.error)}

); } return this.props.children; } } ``` ### Retry Logic with Exponential Backoff Implement smart retry logic for transient errors: ```typescript theme={null} import { ScuteAuth } from "@scute/js-core"; const authenticateWithRetry = async ( credentials: { email: string; password: string }, maxRetries: number = 3 ) => { let lastError: Error; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const result = await ScuteAuth.signIn(credentials); return result; } catch (error: any) { lastError = error; // Don't retry for certain error types that won't resolve with retries if (error.code === "identifier-not-recognized" || error.code === "identifier-already-exists" || error.code === "identifier-invalid" || error.code === "invalid-magic-link" || error.code === "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED" || error.code === "ERROR_INVALID_DOMAIN" || error.code === "ERROR_INVALID_RP_ID") { throw error; } // Don't retry on the last attempt if (attempt === maxRetries) { break; } // Only retry for network errors and server errors const shouldRetry = [502, 503, 504].includes(error.code) || error.code === "ERROR_CEREMONY_ABORTED" || error instanceof TechnicalError; if (!shouldRetry) { throw error; } // Exponential backoff: wait 2^attempt seconds const delay = Math.pow(2, attempt) * 1000; console.log(`Authentication attempt ${attempt} failed, retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; }; ``` ### Form Validation with Error Display Create a comprehensive form handler with error display: ```typescript theme={null} import { useState } from 'react'; import { ScuteAuth, getMeaningfulError } from "@scute/js-core"; const useAuthForm = () => { const [errors, setErrors] = useState>({}); const [isLoading, setIsLoading] = useState(false); const [generalError, setGeneralError] = useState(""); const validateField = (name: string, value: string): string => { switch (name) { case 'email': if (!value) return 'Email is required'; if (!/\S+@\S+\.\S+/.test(value)) return 'Email format is invalid'; return ''; case 'password': if (!value) return 'Password is required'; if (value.length < 8) return 'Password must be at least 8 characters'; return ''; default: return ''; } }; const handleSubmit = async (formData: { email: string; password: string }) => { // Clear previous errors setErrors({}); setGeneralError(""); // Validate all fields const fieldErrors: Record = {}; Object.entries(formData).forEach(([key, value]) => { const error = validateField(key, value); if (error) fieldErrors[key] = error; }); if (Object.keys(fieldErrors).length > 0) { setErrors(fieldErrors); return; } setIsLoading(true); try { await authenticateWithRetry(formData); // Handle successful authentication } catch (error: any) { const errorResult = getMeaningfulError(error); // Handle field-specific errors if (error.code === "identifier-invalid") { setErrors({ email: "Please enter a valid email address" }); } else if (error.code === "identifier-not-recognized") { setErrors({ email: "This email address is not recognized" }); } else { // Handle general errors setGeneralError(errorResult.message || "Authentication failed. Please try again."); } // Log error for debugging console.error("Form submission error:", { error: errorResult.message, isFatal: errorResult.isFatal, code: error.code, formData: { email: formData.email } // Don't log password }); } finally { setIsLoading(false); } }; return { errors, generalError, isLoading, handleSubmit, validateField }; }; ``` ## Error Monitoring and Logging ### Production Error Tracking Set up comprehensive error tracking for production environments: ```typescript theme={null} interface ErrorContext { userId?: string; sessionId?: string; userAgent: string; url: string; timestamp: string; } const logAuthError = (error: any, context: Partial = {}) => { const errorResult = getMeaningfulError(error); const errorData = { message: errorResult.message, isFatal: errorResult.isFatal, code: error.code, stack: error.stack, context: { userAgent: navigator.userAgent, url: window.location.href, timestamp: new Date().toISOString(), ...context } }; // Log to console in development if (process.env.NODE_ENV === 'development') { console.error('Auth Error:', errorData); } // Send to monitoring service in production if (process.env.NODE_ENV === 'production') { // Example: Send to your monitoring service fetch('/api/errors', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(errorData) }).catch(err => { console.error('Failed to log error:', err); }); } }; ``` ## Best Practices ### 1. User-Friendly Messages Always provide clear, actionable error messages to users: ```typescript theme={null} // ❌ Bad: Technical error exposed to user setErrorMessage("JWT token validation failed: signature mismatch"); // ✅ Good: User-friendly message with clear action setErrorMessage("Your session has expired. Please sign in again."); ``` ### 2. Security Considerations Never expose sensitive information in error messages: ```typescript theme={null} // ❌ Bad: Reveals whether email exists if (userNotFound) { throw new Error("User with email john@example.com not found"); } // ✅ Good: Generic message for security if (invalidCredentials) { throw new Error("Invalid email or password"); } ``` ### 3. Graceful Degradation Provide fallback functionality when possible: ```typescript theme={null} const handleAuthError = (error: any) => { if (error.code === "NETWORK_ERROR") { // Offer offline mode or cached data setOfflineMode(true); setErrorMessage("You're offline. Some features may be limited."); } }; ``` ### 4. Error Recovery Always provide users with a path to recover from errors: ```typescript theme={null} const ErrorDisplay = ({ error, onRetry, onReset }) => (

{getMeaningfulError(error)}

Forgot Password?
); ``` ## Testing Error Scenarios Create comprehensive tests for your error handling: ```typescript theme={null} import { describe, it, expect, vi } from 'vitest'; import { getMeaningfulError } from '@scute/js-core'; describe('Error Handling', () => { it('should handle identifier not recognized error', () => { const error = { code: 'identifier-not-recognized', message: 'Identifier is not recognized.' }; const result = getMeaningfulError(error); expect(result.message).toBe('Identifier is not recognized.'); expect(result.isFatal).toBe(false); }); it('should handle network errors as fatal', () => { const error = { code: 502, message: 'Bad Gateway' }; const result = getMeaningfulError(error); expect(result.isFatal).toBe(true); }); it('should handle WebAuthn ceremony aborted as non-fatal', () => { const error = { code: 'ERROR_CEREMONY_ABORTED', message: 'Ceremony aborted' }; const result = getMeaningfulError(error); expect(result.isFatal).toBe(false); }); it('should handle technical errors as fatal', () => { const technicalError = new TechnicalError(); const result = getMeaningfulError(technicalError); expect(result.isFatal).toBe(true); expect(result.message).toBe('Something went wrong.'); }); it('should handle custom scute errors as non-fatal', () => { const error = { code: 'invalid-magic-link', message: 'Invalid magic link.' }; const result = getMeaningfulError(error); expect(result.message).toBe('Invalid magic link.'); expect(result.isFatal).toBe(false); }); }); ``` By implementing comprehensive error handling following these patterns, you'll create a robust authentication system that provides excellent user experience even when things go wrong. # Scute Events Source: https://docs.scute.io/guides/events # Listening for Auth Events Scute provides a way to listen for auth events. This is useful if you want to know when a user has signed up, signed in, or signed out. To listen for auth events, you can use the `onAuthStateChange` method of the `ScuteClient` instance. This method takes a callback function that will be called with the auth event object. The `onAuthStateChange` method returns an unsubscribe function that you can use to stop listening for auth events. ```javascript theme={null} import { AUTH_CHANGE_EVENTS, ... } from "@scute/core"; ... const [authState, setAuthState] = useState(); useEffect(() => { const unsubscribe = scuteClient.onAuthStateChange( (event, session, user) => { console.log(event, session, user); } ); return () => unsubscribe(); }, [scuteClient]); ... ``` The `onAuthStateChange` callback function will be called with the following arguments: 1. `event`: Name of the auth event. 2. `session`: The session object. 3. `user`: The user object. The `event` argument will be one of the following values: * `AUTH_CHANGE_EVENTS.SIGNED_IN`: The user has signed in. * `AUTH_CHANGE_EVENTS.SIGNED_OUT`: The user has signed out. * `AUTH_CHANGE_EVENTS.INITIAL_SESSION`: Fired when the initial session (or SDK) is loaded. * `AUTH_CHANGE_EVENTS.SESSION_REFETCHED`: Fired when the session is refetched. * `AUTH_CHANGE_EVENTS.SESSION_EXPIRED`: Fired when the session has expired. * `AUTH_CHANGE_EVENTS.TOKEN_REFRESHED`: Fired when the token is refreshed. * `AUTH_CHANGE_EVENTS.MAGIC_PENDING`: Fired when magick link token verification is pending. * `AUTH_CHANGE_EVENTS.MAGIC_NEW_DEVICE_PENDING`: Fired when magick link is verified and webauthn device register is available. * `AUTH_CHANGE_EVENTS.MAGIC_VERIFIED`: Fired when magick link token verification is successful. * `AUTH_CHANGE_EVENTS.WEBAUTHN_REGISTER_STARTED`: Fired when WebAuthn device registration starts. * `AUTH_CHANGE_EVENTS.WEBAUTHN_REGISTER_SUCCESS`: Fired when WebAuthn device registration completes successfully. * `AUTH_CHANGE_EVENTS.WEBAUTHN_REGISTER_ERROR`: Fired when WebAuthn device registration encounters an error. * `AUTH_CHANGE_EVENTS.WEBAUTHN_VERIFY_START`: Fired when WebAuthn device verification starts for a previously registered device. * `AUTH_CHANGE_EVENTS.WEBAUTHN_VERIFY_SUCCESS`: Fired when WebAuthn sign-in completes successfully. * `AUTH_CHANGE_EVENTS.OTP_PENDING`: Fired when a one time code (OTP) is sent and pending to be verified. * `AUTH_CHANGE_EVENTS.OTP_NEW_DEVICE_PENDING`: Fired when OTP is verified and webauthn device register is available. # Intent Verification Source: https://docs.scute.io/guides/intent-verification Verify user identity via email or SMS with workflow context # 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. ```bash theme={null} 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 ```bash theme={null} 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} ``` | Field | Required | Description | | -------------------------- | --------- | -------------------------------------------------------------- | | `intent_name` | yes | e.g. "Password Reset", "Approve Ticket" | | `method` | no | `email` (default) or `sms` | | `verification_type` | no | `magic_link` or `otp` | | `redirect_url` | no | Where to send the user after verification | | `meta_data.contact_email` | for email | Recipient email | | `meta_data.contact_phones` | for sms | `[{ "phone_number": "+15551234567", "phone_type": "mobile" }]` | | `meta_data.contact_name` | no | Display name | | `meta_data.ticket_id` | no | Your reference ID — returned in webhooks and JWT | | `meta_data.contact_id` | no | Your user ID — returned in webhooks and JWT | Add any extra fields to `meta_data` — they're all echoed back. ### Email magic link ```javascript theme={null} 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 ```javascript theme={null} 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: ```json theme={null} { "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. | Event | When | | -------------------------------- | ----------------------------- | | `verification.*.requested` | Sent | | `verification.*.verified` | Confirmed | | `verification.*.expired` | Timed out | | `verification.*.failed` | OTP max attempts | | `verification.*.denied` | Contact denied (consent mode) | | `verification.*.delivery_failed` | SMS/email didn't send | Replace `*` with `email` or `sms`. Payload: ```json theme={null} { "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 | Action | Endpoint | | ------------ | ---------------------------------------------------- | | Check status | `GET /v1/verify/{app_id}/verifications/{id}` | | Cancel | `DELETE /v1/verify/{app_id}/verifications/{id}` | | Resend | `POST /v1/verify/{app_id}/verifications/{id}/resend` | | Deny | `POST /v1/verify/{app_id}/verifications/{id}/deny` | *** ## Settings | Setting | Default | | --------------------- | ----------------------------------------------- | | Tenant mode | `full` or `verify_only` | | Verification mode | `dismiss` or `consent` | | Redirect URL template | — | | Redirect delay | 4s | | Success message | "Your identity has been successfully verified." | | Rate limit | 10/hour per contact | *** ## Testing [Scute Testbench](https://testbench.scute.io) — connect with M2M token, search users, send verifications, watch webhooks live. Local webhooks: `ngrok http 3000` # JWT Source: https://docs.scute.io/guides/jwt A comprehensive guide for validating Scute authentication tokens in your application. ## Understanding Scute's Structure ### The Basics Scute organizes users and permissions in a simple hierarchy: ``` Workspace (Your Organization) ├── Apps (Your Applications) │ └── App Users (Your End Users) └── Users (Workspace Members) ``` **Example:** * **Workspace:** "Acme Corp" (your company) * **App:** "Acme Mobile App" (your customer-facing app) * **App Users:** Your end customers ([john@customer.com](mailto:john@customer.com), [jane@customer.com](mailto:jane@customer.com)) * **Users:** Your team members ([admin@acme.com](mailto:admin@acme.com), [dev@acme.com](mailto:dev@acme.com)) ### Key Concepts * **Workspace:** Your organization's account on Scute * **App:** Each application you build (mobile app, web app, etc.) * **App Users:** Your end customers who log into your apps * **Users:** Your team members who manage the workspace ## JWT Token Structure When a user successfully authenticates, Scute issues a JWT token with the following structure: ```json theme={null} { "uuid": "app_user_id", // The authenticated user ID "aid": "your_app_id", // Your application ID "uid": "session_id", // Unique session identifier "wid": "workspace_id", // Your workspace ID "crid": "credential_id", // Webauthn credential id (optional) "exp": 1640995200, // Token expiration (Unix timestamp) "iat": 1640991600 // Token issued at (Unix timestamp) } ``` ### Token Claims Explained | Claim | Description | Example | | ------ | ------------------------------------------------------------ | ----------------- | | `uuid` | App User ID - identifies the authenticated user | `"usr_1234abcd"` | | `aid` | App ID - your application identifier | `"app_5678efgh"` | | `uid` | Session ID - unique identifier for this login session | `"sess_9012ijkl"` | | `wid` | Workspace ID - your organization's identifier | `"ws_3456mnop"` | | `crid` | Credential ID - method used to authenticate (WebAuthn, etc.) | `"cred_7890qrst"` | | `exp` | Expiration - when the token expires | `1640995200` | | `iat` | Issued At - when the token was created | `1640991600` | ## Offline JWT Verification (Recommended) Offline verification is faster and more scalable as it doesn't require API calls to validate tokens. ### Step 1: Get the Public Key Fetch your app's public key for JWT verification (this is a **public endpoint**): ```javascript theme={null} // Get RSA public key in PEM format const response = await fetch(`https://api.scute.io/v1/auth/${app_id}/public_key`); const { public_key, algorithm, key_id } = await response.json(); console.log(public_key); // -----BEGIN PUBLIC KEY----- // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... // -----END PUBLIC KEY----- ``` **Response Format:** ```json theme={null} { "public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG...\n-----END PUBLIC KEY-----", "algorithm": "RS256", "key_id": "ws_1234_1640991600" } ``` ### Step 2: Offline JWT Verification ```javascript theme={null} import jwt from 'jsonwebtoken'; // Your session token from user login const accessToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."; try { // Verify the token offline const decoded = jwt.verify(accessToken, public_key, { algorithms: ['RS256'] // Note: Current tokens don't include standard 'aud' or 'iss' claims // Audience validation is done manually below using the 'aid' claim }); // Manually validate that token is for your app if (decoded.aid !== your_app_id) { throw new Error('Token not intended for this app'); } console.log('✅ Token is valid!'); console.log('User ID:', decoded.uuid); console.log('App ID:', decoded.aid); console.log('Session ID:', decoded.uid); // Token is valid - user is authenticated return { authenticated: true, userId: decoded.uuid, sessionId: decoded.uid, expiresAt: new Date(decoded.exp * 1000) }; } catch (error) { console.log('❌ Token is invalid:', error.message); return { authenticated: false, error: error.message }; } ``` ### Step 3: Using JWKS (Recommended for Production) JWKS (JSON Web Key Set) provides automatic key rotation support: ```javascript theme={null} import jwksClient from 'jwks-rsa'; // Create JWKS client (handles caching and key rotation) const client = jwksClient({ jwksUri: `https://api.scute.io/v1/auth/${app_id}/jwks`, cache: true, cacheMaxAge: 3600000, // Cache for 1 hour rateLimit: true, jwksRequestsPerMinute: 5 }); // Get signing key function function getKey(header, callback) { client.getSigningKey(header.kid, (err, key) => { if (err) { callback(err); return; } const signingKey = key.getPublicKey(); callback(null, signingKey); }); } // Verify token with automatic key management jwt.verify(accessToken, getKey, { algorithms: ['RS256'] }, (err, decoded) => { if (err) { console.log('❌ Token invalid:', err.message); return; } console.log('✅ Token valid:', decoded); // Proceed with authenticated user }); ``` ### Key Points for Offline Validation #### ✅ Benefits * **🚀 Performance:** No API calls required for each validation * **🔒 Security:** Cryptographically secure verification * **⚡ Scalability:** No rate limits on token validation * **🌐 Offline Support:** Works without internet connectivity #### ⚠️ Important Considerations 1. **Cache Public Keys Wisely** ```javascript theme={null} // Good: Cache for reasonable time const cacheTime = 1000 * 60 * 60; // 1 hour // Bad: Cache forever (keys may rotate) // Don't cache indefinitely ``` 2. **Validate Token Claims** ```javascript theme={null} // Ensure token is for your app (manual audience validation) if (decoded.aid !== your_app_id) { throw new Error('Token not intended for this app'); } // Verify token hasn't expired if (decoded.exp < Math.floor(Date.now() / 1000)) { throw new Error('Token has expired'); } ``` 3. **Handle Key Rotation** ```javascript theme={null} // Use JWKS for automatic key rotation // or refresh public keys every hour ``` ## Online Verification (Alternative) If you prefer server-side validation or need additional user data, use online verification: ### Endpoint ``` GET https://api.scute.io/v1/auth/{app_id}/current_user Authorization: Bearer {access_token} ``` ### Example Implementation ```javascript theme={null} async function verifyTokenOnline(accessToken, appId) { try { const response = await fetch(`https://api.scute.io/v1/auth/${appId}/current_user`, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const userData = await response.json(); return { authenticated: true, user: userData.user, // Additional user data available: // - email, phone, name // - verification status // - user metadata }; } catch (error) { console.log('❌ Online verification failed:', error.message); return { authenticated: false, error: error.message }; } } // Usage const result = await verifyTokenOnline(accessToken, 'your_app_id'); if (result.authenticated) { console.log('User data:', result.user); } else { console.log('Authentication failed:', result.error); } ``` ### Online vs Offline Verification | Method | Speed | Data Available | Internet Required | Rate Limits | | ----------- | --------- | ----------------- | ----------------- | --------------------- | | **Offline** | ⚡ Fast | Basic claims only | ❌ No | ❌ None | | **Online** | 🐌 Slower | Full user data | ✅ Yes | ✅ Standard API limits | ## Best Practices ### Security Recommendations 1. **Use HTTPS Only:** Never transmit tokens over HTTP 2. **Validate Audience:** Always check the `aid` claim matches your app 3. **Check Expiration:** Validate `exp` claim before processing 4. **Store Securely:** Use secure storage for tokens on client-side ### Performance Optimization 1. **Cache Public Keys:** Refresh every 1-6 hours 2. **Use Offline Verification:** For high-traffic applications 3. **Connection Pooling:** Reuse HTTP connections for online verification 4. **Error Handling:** Gracefully handle network failures ### Example: Complete Verification Function ```javascript theme={null} class ScuteJWTVerifier { constructor(appId) { this.appId = appId; this.publicKey = null; this.keyLastFetched = null; this.cacheTime = 3600000; // 1 hour } async getPublicKey() { const now = Date.now(); // Use cached key if still valid if (this.publicKey && this.keyLastFetched && (now - this.keyLastFetched) < this.cacheTime) { return this.publicKey; } // Fetch fresh public key const response = await fetch(`https://api.scute.io/v1/auth/${this.appId}/public_key`); const data = await response.json(); this.publicKey = data.public_key; this.keyLastFetched = now; return this.publicKey; } async verifyToken(token) { try { const publicKey = await this.getPublicKey(); const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] }); // Manually validate audience (app ID) if (decoded.aid !== this.appId) { throw new Error('Token not intended for this app'); } return { valid: true, userId: decoded.uuid, sessionId: decoded.uid, expiresAt: new Date(decoded.exp * 1000) }; } catch (error) { return { valid: false, error: error.message }; } } } // Usage const verifier = new ScuteJWTVerifier('your_app_id'); const result = await verifier.verifyToken(userToken); if (result.valid) { console.log(`User ${result.userId} is authenticated`); } else { console.log(`Authentication failed: ${result.error}`); } ``` ## Troubleshooting ### Common Issues **"Invalid signature" Error** * Check that you're using the correct public key for your app * Ensure you're using the RS256 algorithm * Verify the token hasn't been modified **"Token expired" Error** * Check system clock synchronization * Token has a limited lifetime (typically 1 hour) * Request a new token using refresh flow **"Invalid audience" Error** * Ensure the `aid` claim matches your app ID * Token might be intended for a different app **Network Errors (Online Verification)** * Check API endpoint URL is correct * Verify internet connectivity * Ensure proper Authorization header format # Metafields Source: https://docs.scute.io/guides/metafields Metafields are custom fields that you can use to store additional information about your users. They are administration-only fields that allow you to add metadata to hold IDs and make associations with your own systems. Scute supports 7 data types for metafields: string, integer, date, email, phone, url, and boolean. Metafields are not used in login forms for collecting extra data from users. Instead, they serve as a way to store and retrieve meta information that you can process in your application during authentication, signup, and other user-related operations. ## Metafield Verification You can now verify metafield values using OTP codes sent to the specified identifier. This is particularly useful for: * Verifying email or phone metafields before important operations * Confirming ownership of contact information stored in metafields * Adding an extra layer of security for sensitive metafield updates See the [User Identifier Verification guide](/docs/verification/user-identifier-verification) for details on implementing metafield verification in your application. Go to [control.scute.io](https://control.scute.io), select your app and click on the "Metafields" tab. Enter the field name, select the data type and click on the "Add +" button to create a new metafield. Metafield types configuration

Metafield update interface # oAuth Source: https://docs.scute.io/guides/oauth OAuth is an open-standard authorization protocol that allows third-party services to exchange your information without exposing your password. In the context of this documentation, OAuth will enable your application to authenticate users via third-party services like Google. This flow typically involves redirecting users to the OAuth provider, where they log in, and then redirecting them back to your application with an authentication token. Incorporating these suggestions could help make the document more accessible, especially to readers who may not be as familiar with OAuth or the specific SDK being used. The JavaScript SDK provides a pre-built UI for OAuth flows, as well as methods to start and authenticate OAuth flows that you can connect to your own UI. Use either of these approaches to quickly get up and running with OAuth. ### Configuration To configure OAuth, sign in to [control.scute.io](https://control.scute.io), and select your app. From the menu on the left hand side, click on Auth Providers, then select the provider you want to configure and enable it. Enter your `Client ID` and `Client Secret` from your provider and copy the `Callback URL` and paste it to the appropriate area for your oAuth provider’s configuration. No session

### Using the JS Core SDK to integrate into your own UI JS Core SDK provides the oAuth configuration and methods to start and oAuth flows that you can connect to your own UI. #### Getting the oAuth configuration: Your `appData` has a property called `oauth_providers` which is an array of configured oAuth providers that are [enabled and configured](#configuration) from [control.scute.io](https://control.scute.io). You can access your app data using the `scuteClient.getAppData` method: ```jsx theme={null} // ... Redacted await {data: appData, error}scuteClient.getAppData(); console.log(appData.oauth_providers); // [ // { // "id": "google", // "provider": "google", // "name": "Google", // "icon": "provider-icons/google-icon.svg" // } // ] ``` This will return an array of objects with the following properties: | Property | Type | Description | | -------------- | ------ | --------------------------- | | id \| provider | string | The provider ID | | name | string | The provider name | | icon | string | A URL for the provider icon | Using this property you can render the oAuth buttons in your UI: ```jsx theme={null} // ... Redacted const [oauthProviders, setOauthProviders] = useState([]); useEffect(() => { const getAppData = async () => { const { data, error } = await scuteClient.getAppData(); if (error) { console.error(error); return; } setOauthProviders(data.oauth_providers || []); }; getAppData(); }, []); return (
{oauthProviders.map((provider) => ( ))}
); // ... Redacted ``` #### Starting the oAuth flow with `signInWithOAuth` method: To start the oAuth flow, you can use the `signInWithOAuth` method of the scute client. This method takes the `provider` ID as an argument and will redirect the user to the oAuth provider's login page: ```jsx theme={null} ``` Your provider and scute api will handle the rest of the flow and redirect the user back to your app with a magic link. Using the scute client verify the magic link as usual and log the user in based on the response: ```jsx theme={null} const magicLinkToken = scuteClient.getMagicLinkToken(); const { data: tokenPayloads, error } = await scuteClient.verifyMagicLinkToken( magicLinkToken ); if (error) { console.error(error); return; } await scuteClient.signInWithTokenPayload(tokenPayloads.authPayload); ``` # TypeScript Support Source: https://docs.scute.io/guides/typescript-support # TypeScript Support Scute provides comprehensive TypeScript definitions: ```typescript theme={null} import type { ScuteClient, ScuteSession, ScuteUserData, ScuteTokenPayload, AuthResponse, SessionResponse, } from "@scute/js-core"; // Use types for better development experience const handleAuthResponse = (response: AuthResponse) => { if (response.error) { // Handle error return; } // response.data is properly typed console.log(response.data); }; ``` # Verification SDK Source: https://docs.scute.io/guides/verification-sdk Manage verifications from any app using @scute/js-core # Verification SDK List, approve, deny verifications from client-side code. User must be authenticated first. ```bash theme={null} npm install @scute/js-core ``` ```typescript theme={null} import { createClient } from "@scute/js-core"; const scute = createClient({ appId: "your-app-id" }); await scute.signInOrUp("user@example.com"); // scute.verifications is now ready ``` *** ## Methods ### List ```typescript theme={null} const { data } = await scute.verifications.list({ status: "pending" }); // data.verifications: Verification[] ``` ### Get ```typescript theme={null} const { data } = await scute.verifications.getById("id"); ``` ### Approve ```typescript theme={null} await scute.verifications.approve("id"); ``` ### Deny ```typescript theme={null} await scute.verifications.deny("id", "reason"); ``` ### Verify OTP ```typescript theme={null} await scute.verifications.verifyCode("id", "847293"); ``` ### Resend ```typescript theme={null} await scute.verifications.resend("id"); ``` ### Cancel ```typescript theme={null} await scute.verifications.cancel("id"); ``` *** ## Subscribe to new verifications Polls for pending verifications. Returns an unsubscribe function. ```typescript theme={null} const unsub = scute.verifications.onNew((v) => { console.log(v.intent, v.identifier); }); // stop unsub(); ``` Custom interval (default 3s): ```typescript theme={null} scute.verifications.onNew(callback, 5000); ``` *** ## Statuses | Status | Meaning | | ----------- | ------------ | | `pending` | Waiting | | `verified` | Approved | | `denied` | Denied | | `expired` | Timed out | | `failed` | Max attempts | | `cancelled` | Cancelled | *** ## Notes * User must authenticate through Scute before calling any method * Auth headers are handled automatically by the SDK * Users only see verifications sent to them * Works in browser, Electron, React Native — anywhere `@scute/js-core` runs # Webhooks Source: https://docs.scute.io/guides/webhooks # Webhooks Receive real-time notifications when events happen in your app. Signed with HMAC-SHA256. ## Setup ``` POST /v1/apps/{app_id}/webhooks Authorization: Bearer {api_key} ``` ```json theme={null} { "webhook": { "url": "https://your-server.com/webhooks", "event_types": ["verification.sms.verified", "verification.sms.expired"], "retry_limit": 3 } } ``` | Parameter | Type | Required | Description | | ------------- | --------- | -------- | ------------------------------------ | | `url` | string | yes | HTTPS endpoint | | `event_types` | string\[] | no | Events to subscribe to. Default: all | | `retry_limit` | int | no | Retry attempts (0-10). Default: 3 | | `description` | string | no | Label | | `metadata` | object | no | Custom metadata | | `enabled` | bool | no | Default: true | *** ## Payload ``` Content-Type: application/json User-Agent: AppName-Webhooks/1.0 X-Webhook-Signature: t=1713232500,v1=5a3c2b... ``` ```json theme={null} { "id": "uuid", "created_at": "2026-04-16T12:00:00Z", "event_type": "verification.sms.verified", "app_id": "uuid", "data": { ... }, "api_version": "v1" } ``` *** ## Signature verification The `X-Webhook-Signature` header contains a timestamp and HMAC: ``` t=1713232500,v1=5a3c2b... ``` Verify: ```javascript theme={null} const crypto = require("crypto"); function verifyWebhook(payload, header, secret) { const [tPart, sigPart] = header.split(","); const timestamp = tPart.replace("t=", ""); const signature = sigPart.replace("v1=", ""); const expected = crypto .createHmac("sha256", secret) .update(`${timestamp}.${payload}`) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } ``` *** ## Events ### Verification events Pattern: `verification.{channel}.{outcome}` — channel is `email` or `sms`. | Event | When | | -------------------------------- | ------------------------------------------------------------------- | | `verification.*.requested` | Verification sent | | `verification.*.verified` | Contact confirmed (magic link click, OTP code, or consent approval) | | `verification.*.expired` | Timed out. Fires even if the link was never opened | | `verification.*.failed` | OTP max attempts exceeded | | `verification.*.denied` | Contact denied the action (consent mode) | | `verification.*.delivery_failed` | SMS/email failed to send (bad number, provider error) | Verification payloads include: ```json theme={null} { "data": { "verification_id": "uuid", "intent": "Password Reset", "meta_data": { "contact_email": "user@company.com", "ticket_id": "12345", "delivery_method": "sms" }, "timing": { "sent_at": "2026-04-16T01:00:00Z", "opened_at": "2026-04-16T01:00:47Z", "confirmed_at": "2026-04-16T01:00:52Z", "time_to_verify_seconds": 52 }, "device": { "os": "iOS", "browser": "Safari" }, "ip_address": "203.0.113.42" } } ``` Expiration events add `was_opened: true/false`. Consent events add `consent_decision` and `denial_reason`. Delivery failures add `error` and `channel`. ### Auth events | Event | When | | --------------------- | ---------------------------- | | `auth.login.failed` | Login attempt failed | | `challenge.created` | Challenge created (any type) | | `challenge.completed` | Challenge verified | | `challenge.failed` | Challenge failed | ### User events | Event | When | | ------------------------- | ---------------------- | | `user.created` | New user registered | | `user.updated` | User profile updated | | `user.identifier_changed` | Email or phone changed | ### System events | Event | When | | -------------- | ------------------------ | | `webhook.test` | Test ping from dashboard | *** ## Managing endpoints | Action | Method | | ---------- | ------------------------------------------------ | | List | `GET /v1/apps/{app_id}/webhooks` | | Get | `GET /v1/apps/{app_id}/webhooks/{id}` | | Update | `PATCH /v1/apps/{app_id}/webhooks/{id}` | | Delete | `DELETE /v1/apps/{app_id}/webhooks/{id}` | | Test | `POST /v1/apps/{app_id}/webhooks/{id}/test` | | Deliveries | `GET /v1/apps/{app_id}/webhooks/{id}/deliveries` | *** ## Delivery status | Status | Meaning | | ------------ | ----------------------- | | `pending` | Queued | | `processing` | In flight | | `delivered` | 2xx response | | `failed` | Will retry if retryable | *** ## Limits * 10-second timeout per delivery * Max 10 retries with exponential backoff * Respond with 2xx within 5 seconds * 4xx = won't retry. 5xx = will retry. # Workspace Sessions - Cross-App login solution Source: https://docs.scute.io/guides/workspace-sessions Enable seamless user authentication between multiple apps in your workspace # Workspace Sessions Workspace Sessions provide a simple Single Sign-On (SSO) solution for users to seamlessly move between multiple apps within the same workspace without re-authentication. ## Use Cases * **Multi-app ecosystems**: Users authenticated in your main app can access related tools/services * **Marketplace platforms**: Vendors authenticated in the marketplace can access their seller dashboard * **Admin panels**: Users can switch between customer-facing app and admin interface * **Microservices**: Frontend can authenticate users across different backend services ## How It Works The workspace session flow exchanges a user's existing session in one app for a new session in another app within the same workspace: ``` User in App A → Workspace Session API → New session in App B ``` ### Authentication Flow 1. **User is already authenticated** in App A within your workspace 2. **Your backend calls** the workspace session endpoint with: * User's access token (from App A) * Workspace API key (from dashboard) * Target app ID (App B) 3. **Receive new session tokens** for the user in App B 4. **User is now authenticated** in App B without manual login ## Implementation ### Prerequisites * Multiple apps in the same workspace * Workspace API key from your Scute dashboard * User authenticated in source app ### API Request ```bash theme={null} POST https://api.scute.io/v1/workspaces/WORKSPACE_ID/sessions Authorization: Bearer X-Authorization: Bearer Content-Type: application/json { "app_id": "app_target_app_id" } ``` ### Response (Scute Session) ```json theme={null} { "access": "eyJhbGciOiJSUzI1NiJ9...", "refresh": "eyJhbGciOiJSUzI1NiJ9...", "csrf": "ttRd+JqyT...", "access_expires_at": "2025-07-11T18:46:43.000+03:00", "refresh_expires_at": "2025-07-18T17:46:43.000+03:00", "user_id": "90c58919-b3f0-450b-8d3d-c57e4f416970", "key_id": "43c742a-9e4c-4a4d-92e7-67ee2e028e55_1750984640" } ``` ## Example: Marketplace to Seller Dashboard ```javascript theme={null} // User clicks "Go to Seller Dashboard" in marketplace async function redirectToSellerDashboard(userToken, workspaceId) { try { const response = await fetch(`/v1/workspaces/${workspaceId}/sessions`, { method: 'POST', headers: { 'Authorization': `Bearer ${WORKSPACE_API_KEY}`, 'X-Authorization': `${userToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ app_id: 'app id' }) }); const tokens = await response.json(); // Redirect to seller dashboard with new tokens // You now have: tokens.access, tokens.refresh, tokens.csrf, tokens.user_id, tokens.key_id, store it somewhere secure window.location.href = `https://sellers.marketplace.com`; } catch (error) { console.error('SSO failed:', error); } } ``` ## Security Considerations ### Access Control * **Workspace API key** ensures only authorized apps can create cross-app sessions * **User token validation** confirms the user exists and has valid authentication * **App workspace validation** prevents cross-workspace token exchange ### Token Scope * Tokens are **app-specific** - cannot be used across different apps * Each workspace uses **separate RSA keys** for token signing * Sessions maintain **user context** but are scoped to the target app ### Error Handling | Status | Error | Meaning | | ------ | ---------------------------- | ---------------------------- | | 401 | `workspace_api_key_required` | Invalid workspace API key | | 403 | `workspace_access_denied` | User not member of workspace | | 404 | `app_not_in_workspace` | Target app not in workspace | ## Best Practices 1. **Store workspace API key securely** - Never expose in client-side code 2. **Validate user permissions** - Ensure user should have access to target app 3. **Handle errors gracefully** - Fallback to standard login if SSO fails 4. **Use HTTPS only** - Protect tokens in transit 5. **Implement token refresh** - Handle token expiration seamlessly ## Getting Your Workspace API Key 1. Go to your [Scute Dashboard](https://dashboard.scute.io) 2. Navigate to your workspace settings 3. Generate or copy your Workspace API Key 4. Store it securely in your backend environment variables # Get started with Scute Source: https://docs.scute.io/index Passwordless authentication for modern applications

Get started with Scute

Add passwordless auth to your app in minutes. WebAuthn, Magic Links, OTP, OAuth

## Let's create a Scute app Signup and create a workspace Create an app and give it a name Easily create Secret keys from your dashboard ## Quick Start Choose your framework to get started: Server-side rendering with React Single-page applications Vanilla JavaScript integration Node.js backend integration Vue.js apps Svelte apps Mobile apps ## Core Features WebAuthn, Magic Links, OTP, and OAuth — all passwordless Drop-in React components for login flows Secure token-based session management Social login with Google, GitHub, and more ## Verification
Verify user identity and intent with OTP codes, email verification, and self-serve flows.
How verification works One-time password flows User-initiated verification ## Platform Real-time event notifications Track auth events and user activity Store custom data on users ## API Reference Full API reference with examples for managing users, apps, and authentication # Express.js Source: https://docs.scute.io/quickstarts/express-js # Backend integration with Express JS Scute can be integrated into an Express.js application using the `@scute/node` package. This package provides a middleware and a method called `authenticateRequest` that can be used to handle the authentication and authorization of users in your Express.js application. Head over to the [example project repo](https://github.com/scuteai/js/tree/main/examples/with-nodejs) to run this example project and check out the [type docs](https://scute-js-docs.netlify.app/) for more `scuteClient` methods. ### Install the `@scute/node` package First, install the `@scute/node` package using npm or yarn: ```bash theme={null} npm install @scute/node ``` ### Initialize the Scute client In your entry point (usually `app.ts`) initialize the Scute client using the `createClient` method exposed by the `@scute/node` package: ```ts theme={null} // app.ts // ... Redacted import { createClient, authenticateRequest, scuteAuthMiddleware, type AuthenticatedRequest, InvalidAuthTokenError, } from "@scute/node"; const scute = createClient({ appId: process.env.SCUTE_APP_ID as string, baseUrl: process.env.SCUTE_BASE_URL as string, }); // ... Redacted middleware and routes ``` ### Using the Scute middleware You can use the `scuteAuthMiddleware` to protect routes that require authentication. The middleware will check if the user is authenticated and attach the user object to the request object. ```ts theme={null} // app.ts // ... Redacted import { createClient, authenticateRequest, scuteAuthMiddleware, type AuthenticatedRequest, InvalidAuthTokenError, } from "@scute/node"; const scute = createClient({ appId: process.env.SCUTE_APP_ID, baseUrl: process.env.SCUTE_BASE_URL, }); // ... Redacted middleware and routes app.get( "/authenticated-route-middleware", scuteAuthMiddleware(scute), async (req, res) => { const user = (req as AuthenticatedRequest).user; res.send(user); } ); ``` ### Using the `authenticateRequest` helper method You can also use the `authenticateRequest` method to authenticate the user manually in your route handlers: ```ts theme={null} // app.ts // ... Redacted import { createClient, authenticateRequest, scuteAuthMiddleware, type AuthenticatedRequest, InvalidAuthTokenError, } from "@scute/node"; const scute = createClient({ appId: process.env.SCUTE_APP_ID, baseUrl: process.env.SCUTE_BASE_URL, }); // ... Redacted middleware and routes app.get("/authenticated-route", async (req, res) => { try { const user = await authenticateRequest(scute, req); res.send(user); } catch (error) { if (error instanceof InvalidAuthTokenError) { res.status(401).send("Unauthorized"); } else { res.status(500).send("Internal Server Error"); } } }); ``` # Javascript Source: https://docs.scute.io/quickstarts/javascript # Integration with Scute JS Core Scute provides a comprehensive authentication solution for web applications through the `@scute/js-core` package. This guide covers implementing multiple authentication methods including passkeys (WebAuthn), magic links, OTP verification, and OAuth providers. While the examples use React, the same patterns and API calls work with any web framework including Vue, Angular, Svelte, or vanilla JavaScript. ## Example Application A complete working example is available in the [Scute JS repository](https://github.com/scuteai/js/tree/main/examples/with-react-and-core). This example demonstrates all the authentication flows covered in this guide, including: * **Multi-method Authentication**: Passkey, magic link, OTP, and OAuth sign-in * **Complete User Interface**: Forms for each authentication method with proper state management * **Session Management**: User profile display with active session management * **Device Registration**: WebAuthn device registration flow with skip option * **Error Handling**: Comprehensive error handling with user-friendly messages * **URL Management**: Proper handling of magic link callbacks and URL cleanup The example app uses Vite + React + TypeScript and provides a practical reference implementation that you can adapt for your own web applications regardless of framework. You can clone the repository and run the example locally to see all authentication flows in action. ## Prerequisites * Node.js 16+ and npm/yarn/pnpm * A Scute application configured in your dashboard * Basic knowledge of JavaScript/TypeScript and your chosen web framework ## Installation Install the Scute JS Core package: ```bash theme={null} npm install @scute/js-core ``` For React applications, also install React dependencies: ```bash theme={null} npm install react react-dom npm install -D @types/react @types/react-dom # For TypeScript projects ``` For other frameworks, install the appropriate dependencies for your chosen framework (Vue, Angular, Svelte, etc.). ## Quick Start ### 1. Client Configuration Create a Scute client instance to handle all authentication operations: ```typescript theme={null} // lib/scute.ts import { createClient } from "@scute/js-core"; export const scuteClient = createClient({ appId: process.env.VITE_SCUTE_APP_ID!, baseUrl: process.env.VITE_SCUTE_BASE_URL!, }); ``` ### 2. Environment Variables Configure your environment variables (`.env`): ```env theme={null} VITE_SCUTE_APP_ID=your_app_id_here VITE_SCUTE_BASE_URL=https://your-scute-instance.com ``` > **Note**: The `VITE_` prefix is required for Vite-based projects. Adjust the prefix according to your build tool (e.g., `REACT_APP_` for Create React App). ### 3. Basic Authentication Flow Implement a complete authentication system with state management. The following example uses React, but the same authentication logic applies to any web framework: ```typescript theme={null} import React, { useState, useEffect } from "react"; import { ScuteClient, type ScuteTokenPayload, type ScuteUserData, getMeaningfulError, } from "@scute/js-core"; import { scuteClient } from "./lib/scute"; function App() { const [currentView, setCurrentView] = useState("loading"); const [user, setUser] = useState(null); useEffect(() => { checkAuthStatus(); handleMagicLinkCallback(); }, []); const checkAuthStatus = async () => { const { data, error } = await scuteClient.getSession(); if (error) { console.error("Session check failed:", error); setCurrentView("login"); return; } if (data?.session?.status === "authenticated") { setUser(data.user); setCurrentView("dashboard"); } else { setCurrentView("login"); } }; const handleMagicLinkCallback = () => { const magicToken = scuteClient.getMagicLinkToken(); if (magicToken) { setCurrentView("verifying"); verifyMagicLink(magicToken); } }; // Render different views based on current state return (
{currentView === "loading" && } {currentView === "login" && } {currentView === "dashboard" && } {currentView === "verifying" && }
); } ``` ## Authentication Flows Scute supports three main authentication flows, each designed for different use cases and user preferences. All flows integrate seamlessly with passkey (WebAuthn) authentication when devices are registered. ### 1. Magic Link Flow The magic link flow allows users to authenticate via email without remembering passwords. When a user enters their email address, a secure magic link is sent to their inbox. Upon clicking the link, they are automatically signed in or signed up if they don't have an account. #### Initial Login Attempt The `signInOrUp` method first attempts passkey authentication. If the user has a registered device, the browser will prompt them to use their passkey instead of sending a magic link. If the user does not have a registered device, the method will send a magic link to the user's email address: ```typescript theme={null} const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const { data, error } = await scuteClient.signInOrUp(identifier); if (error) { console.log("signInOrUp error"); return console.log({ data, error, meaningfulError: error && getMeaningfulError(error), }); } if (!data) { // Passkey verified successfully - user has registered device setComponent("profile"); } else { setComponent("magic_sent"); } }; ``` #### Sending Magic Links You can also force sending a magic link regardless of registered devices: ```typescript theme={null} const handleSendMagicLink = async () => { await scuteClient.sendLoginMagicLink(identifier); setComponent("magic_sent"); }; ``` #### Magic Link Confirmation After sending the magic link, show a confirmation message: ```typescript theme={null} const MagicSent = ({ identifier }: { identifier: string }) => { return (
Magic Link Sent

Please check {identifier} for the magic link.

); }; ``` #### Magic Link Verification When users click the magic link, your application automatically detects and verifies the token: ```typescript theme={null} // Detect magic link token on app load useEffect(() => { const magicLinkToken = scuteClient.getMagicLinkToken(); if (magicLinkToken) { setComponent("magic_verify"); setMagicLinkToken(magicLinkToken); } }, [scuteClient]); // Verification component const MagicVerify = ({ scuteClient, setComponent, magicLinkToken, setTokenPayload, }: { scuteClient: ScuteClient; setComponent: (component: string) => void; magicLinkToken: string | null; setTokenPayload: (tokenPayload: ScuteTokenPayload | null) => void; }) => { const url = new URL(window.location.href); const shouldSkipDeviceRegistration = !!url.searchParams.get(SCUTE_SKIP_PARAM); const verificationStarted = useRef(false); useEffect(() => { const verifyMagicLink = async () => { if (!magicLinkToken) { return console.log("no magic link token found"); } const { data, error } = await scuteClient.verifyMagicLinkToken( magicLinkToken ); if (error) { console.log("verifyMagicLink error"); return console.log({ data, error, meaningfulError: error && getMeaningfulError(error), }); } setTokenPayload(data.authPayload); setComponent("register_device"); url.searchParams.delete(SCUTE_MAGIC_PARAM); window.history.replaceState({}, "", url.toString()); }; if (!verificationStarted.current) { verificationStarted.current = true; verifyMagicLink(); } }, []); return (
Verifying Magic Link...
); }; ``` #### Device Registration After successful magic link verification, users can be prompted to register their device for future passkey authentication. Users can also be given the option to skip device registration and proceed directly to their profile. This enables passwordless login using WebAuthn for subsequent authentications: ```typescript theme={null} export const RegisterDevice = ({ scuteClient, tokenPayload, setComponent, }: { scuteClient: ScuteClient; tokenPayload: ScuteTokenPayload | null; setComponent: (component: string) => void; }) => { const handleRegisterDevice = async () => { if (!tokenPayload) { console.error("No token payload"); return; } const { error: signInError } = await scuteClient.signInWithTokenPayload( tokenPayload ); if (signInError) { console.log("signInWithTokenPayload error"); console.log({ signInError, meaningfulError: getMeaningfulError(signInError), }); return; } const { data, error } = await scuteClient.addDevice(); if (error) { console.log("addDevice error"); console.log({ data, error, meaningfulError: getMeaningfulError(error) }); return; } setComponent("profile"); }; const handleSkipDeviceRegistration = async () => { if (!tokenPayload) { console.error("No token payload"); return; } const { error: signInError } = await scuteClient.signInWithTokenPayload( tokenPayload ); if (signInError) { console.log("signInWithTokenPayload error"); console.log({ signInError, meaningfulError: getMeaningfulError(signInError), }); return; } setComponent("profile"); }; return (
Register Device
); }; ``` ### 2. OTP Flow The OTP (One-Time Password) flow works with both email addresses and phone numbers. The identifier type can be configured in your Scute dashboard. After entering their identifier, users receive a verification code that must be entered to complete authentication. #### OTP Verification Form ```typescript theme={null} const OtpForm = ({ scuteClient, identifier, setComponent, setTokenPayload, }: { scuteClient: ScuteClient; identifier: string; setComponent: (component: string) => void; setTokenPayload: (tokenPayload: ScuteTokenPayload | null) => void; }) => { const [otp, setOtp] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const { data, error } = await scuteClient.verifyOtp(otp, identifier); if (error) { console.log("verifyOtp error"); console.log({ data, error, meaningfulError: getMeaningfulError(error) }); return; } if (data) { setTokenPayload(data.authPayload); setComponent("register_device"); } }; return (
Enter OTP
setOtp(e.target.value)} />
); }; ``` Like the magic link flow, if the user already has a registered device, the browser will prompt them to use their passkey during the initial login attempt. After successful OTP verification, users are presented with the option to register their device for future passkey authentication. ### 3. OAuth Flow The OAuth flow enables users to authenticate through third-party providers like Google. This flow is unique because after successful third-party authentication, Scute redirects back to your application with both a magic link token and a skip parameter. #### Initiating OAuth Authentication ```typescript theme={null} const handleSignInWithGoogle = async () => { await scuteClient.signInWithOAuthProvider("google"); }; ``` #### OAuth Callback Handling Upon successful authentication with the third-party provider, Scute redirects back to your application with: * A `sct_magic` parameter containing the authentication token * A `sct_sk` parameter that differentiates OAuth magic links from email magic links Your magic verification method can handle OAuth callbacks as well. It also sends a `sct_sk` parameter which you can use to determine the app behaviour based on whether it is an emailed magic link or an OAuth magic link. Below example uses the `sct_sk` parameter to skip the device registration step for OAuth flows. ```typescript theme={null} const shouldSkipDeviceRegistration = !!url.searchParams.get(SCUTE_SKIP_PARAM); if (!shouldSkipDeviceRegistration && data?.authPayload) { // Prompt for device registration (rare for OAuth) setTokenPayload(data.authPayload); setComponent("register_device"); } else { // Direct sign-in (typical for OAuth flows) setComponent("profile"); } ``` The `sct_sk` parameter is OAuth-specific. ## Session Management Handle user sessions and authentication state: ```typescript theme={null} // Get current session const getCurrentSession = async () => { const { data, error } = await scuteClient.getSession(); if (error) { console.error("Failed to get session:", error); return null; } return data; }; // Sign out user const signOut = async () => { try { await scuteClient.signOut(); setUser(null); setCurrentView("login"); } catch (error) { console.error("Sign out failed:", error); } }; // Revoke specific session const revokeSession = async (sessionId: string) => { try { await scuteClient.revokeSession(sessionId); // Refresh user data to update sessions list await refreshUserData(); } catch (error) { console.error("Failed to revoke session:", error); } }; ``` ## Error Handling Implement comprehensive error handling for authentication flows: ```typescript theme={null} import { getMeaningfulError } from "@scute/js-core"; const handleAuthError = (error: any) => { const meaningfulError = getMeaningfulError(error); // Log for debugging console.error("Authentication error:", meaningfulError); // Show user-friendly message setErrorMessage( meaningfulError || "Authentication failed. Please try again." ); // Handle specific error types if (error.code === "INVALID_CREDENTIALS") { // Handle invalid credentials } else if (error.code === "NETWORK_ERROR") { // Handle network issues } }; ``` ## TypeScript Support Scute provides comprehensive TypeScript definitions: ```typescript theme={null} import type { ScuteClient, ScuteSession, ScuteUserData, ScuteTokenPayload, AuthResponse, SessionResponse, } from "@scute/js-core"; // Use types for better development experience const handleAuthResponse = (response: AuthResponse) => { if (response.error) { // Handle error return; } // response.data is properly typed console.log(response.data); }; ``` # Next.js Source: https://docs.scute.io/quickstarts/next-js # Using the Next.js SDK with the pre-built React UI If you would like to use the pre-built `Scute UI` with your Next.js app, you can do so using the `@scute/ui-react` package alongside `@scute/nextjs` and `@scute/react`. This package would expose the pre-built `Auth` and `Profile` components which you can use to easily integrate Scute to your current app. Head over to the [example project repo](https://github.com/scuteai/nextjs-scute-ui) to clone and run this example project and check out the [type docs](https://scute-js-docs.netlify.app/) for more `scuteClient` methods. ### Install the SDK Install our React SDKs with your favorite package manager: `npm install @scute/nextjs @scute/react @scute/ui-react` Add your [credentials](/docs) to your environment variable handler: ```sh theme={null} NEXT_PUBLIC_SCUTE_APP_ID="YOUR_SCUTE_PROJECT_ID" NEXT_PUBLIC_SCUTE_BASE_URL="YOUR_SCUTE_BASE_URL" SCUTE_SECRET="YOUR_SCUTE_SECRET" ``` ### Add the Scute Next.js Handlers (App Router) In your project under `src/app`, create the path `auth/[...scute]`. Inside the newly created folder `[...scute]`, create a file named `route.js`. This file will be the handler for the requests coming in from the pre-built React UI. The directory structure should look like this: ``` src/ └─ app/ └─ auth/ └─ [...scute]/ └─ route.js ``` To implement the handlers, add the code block below to the `route.js` file. ```js theme={null} import { cookies, headers } from "next/headers"; import { ScuteHandler } from "@scute/nextjs"; const handler = ScuteHandler({ cookies, headers }); export { handler as GET, handler as POST }; ``` ### Add the UI and the AuthContextProvider Using the `@scute/ui-react` and `@scute/react` packages, create an `AuthProvider` and the `Auth` component. Please refer to the [Scute React UI docs](./react.mdx) for more information. ```js theme={null} // src/components/authprovider.js "use client"; import { useState } from "react"; import { createClientComponentClient } from "@scute/nextjs"; import { AuthContextProvider } from "@scute/react"; export default function AuthProvider({ children }) { const [scuteClient] = useState(() => createClientComponentClient({ appId: process.env.NEXT_PUBLIC_SCUTE_APP_ID, baseUrl: process.env.NEXT_PUBLIC_SCUTE_BASE_URL, }) ); return ( {children} ); } ``` ```js theme={null} // src/components/auth.js "use client"; import { useRouter } from "next/navigation"; import { useScuteClient } from "@scute/react"; import { Auth as ScuteAuth } from "@scute/ui-react"; export default function Auth() { const router = useRouter(); const scuteClient = useScuteClient(); return ( { router.push("/profile"); }} logoUrl="https://example.com/logo.svg" /> ); } ``` ### Add the `AuthProvider` to the `RootLayout` Wrap the HTML body with the Auth provider inside your root layout file ```js theme={null} // src/app/layout.js import AuthProvider from "@/components/authprovider"; import "./globals.css"; export const metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children }) { return ( {children} ); } ``` Once the `AuthProvider` is in place, go ahead and add the Auth component to your homepage: ```js theme={null} // src/app/page.js import Auth from "@/components/auth"; export default function Home() { return (
); } ``` ### Create a profile page with the `Profile` component Finally, build the profile page to handle the redirect from the home page on successful authentication, ```js theme={null} // src/app/profile/page.js "use client"; import { useScuteClient } from "@scute/react"; import { Profile as ScuteProfile } from "@scute/ui-react"; export default function Profile() { const scuteClient = useScuteClient(); return ; } ``` Congratulations! You now have a working Scute instance inside your Next.js app! ### Add the Scute Next.js Handlers (Pages Router) To add the Scute handlers for Pages Router, create the path `src/pages/auth` and create a file named `[...scute].js` with the below content. ```js theme={null} // src/pages/auth/[...scute].js import { ScuteHandler } from "@scute/nextjs"; export default ScuteHandler; ``` To use Edge runtime, use the following code: ```js theme={null} import { authenticateRequest } from "@scute/edge"; import { createPagesEdgeRuntimeClient } from "@scute/nextjs"; import { type NextRequest, NextResponse } from "next/server"; export const config = { runtime: "edge", }; export default async function handler(request: NextRequest) { const scute = createPagesEdgeRuntimeClient({ request }); const { data: user, error } = await authenticateRequest(request, scute); return NextResponse.json(user); } ``` # React Source: https://docs.scute.io/quickstarts/react Using the pre-built Scute UI with React If you would like to use the pre-built `Scute UI` with your react app, you can do so using the `@scute/ui-react` package. This package would expose the pre-built `Auth` and `Profile` components which you can use to easily integrate Scute to your current app. Head over to the [example project repo](https://github.com/scuteai/react-scute-ui) to clone and run this example project and check out the [type docs](https://scute-js-docs.netlify.app/) for more `scuteClient` methods. To get started, install our React SDKs with your favorite package manager: ```sh title="Terminal" theme={null} npm install @scute/react @scute/ui-react` ``` Add your [credentials](/docs) to your environment variable handler: ```sh theme={null} VITE_SCUTE_APP_ID="YOUR_SCUTE_PROJECT_ID" VITE_SCUTE_BASE_URL="YOUR_SCUTE_BASE_URL" ``` **NOTE**: If you are not using Vite, use "REACT\_APP" as your prefix for your environment variables. ### Initialize the Scute client First initialize the Scute client using the `createClient` method exposed by `@scute/react` package: ```js theme={null} // scute.js import { createClient } from "@scute/react"; export const scute = createClient({ appId: import.meta.env.VITE_SCUTE_APP_ID, baseUrl: import.meta.env.VITE_SCUTE_BASE_URL, }); ``` ### Wrap your React app with Scute AuthContextProvider To be able to use the `useScuteClient` and `useAuth` hooks, wrap your app inside the Scute `AuthContextProvider`: ```jsx theme={null} // App.jsx import { AuthContextProvider } from "@scute/react"; import { scute } from "./scute"; export default function App() { return ( {/* This is where the Prebuilt component and the rest of your application lives */} ); } ``` ### Add the Scute pre-built UI First, create a component to show to your authenticated users: ```jsx theme={null} // AuthenticatedView.jsx import { Profile } from "@scute/ui-react"; import { useAuth, useScuteClient } from "@scute/react"; export const AuthenticatedView = () => { const { session, user, signOut } = useAuth(); const scute = useScuteClient(); if (session.status === "loading") { return null; } else if (session.status === "unauthenticated") { return <>unauthenticated; } return ; }; ``` Then, create a component to switch between the authentication form and the `AuthenticatedView` based on the session status: ```jsx theme={null} // ScuteUI.jsx import { Auth } from "@scute/ui-react"; import { useScuteClient, useAuth } from "@scute/react"; import { AuthenticatedView } from "./AuthenticatedView"; export const ScuteUI = () => { const { session } = useAuth(); const scute = useScuteClient(); if (session.status === "authenticated") { return ; } return ( ); }; ``` Finally, modify the `App.jsx` to render the `ScuteUI` inside the `AuthContextProvider`: ```jsx theme={null} // App.jsx import { AuthContextProvider } from "@scute/react"; import { scute } from "./scute"; import { ScuteUI } from "./ScuteUI"; export default function App() { return ( ); } ``` Congrats! You have a working Scute instance now! **No session** No session


**With session (Profile)** With session


**With session (UserButton)** With session


## Component API's ### `Auth` Component | Property | Type | Default | Description | | ----------- | ------------------------------------ | ---------- | ---------------------------------------------------- | | scuteClient | ScuteClient | undefined | The Scute client instance. This property is required | | onSignIn? | () => void | undefined | Callback function for sign-in | | webauthn? | "strict" \| "optional" \| "disabled" | "optional" | Options for WebAuthn | | language? | string | "en" | Language setting | | appearance? | ? | undefined | Appearance settings | ### `Profile` Component | Property | Type | Default | Description | | ----------- | ----------- | --------- | ------------------------- | | scuteClient | ScuteClient | undefined | The Scute client instance | | language? | string | "en" | Language setting | ### `UserButton` Component | Property | Type | Default | Description | | ------------------ | ----------- | --------- | -------------------------- | | scuteClient | ScuteClient | undefined | The Scute client instance | | language? | string | "en" | Language setting | | username? | string | undefined | Username to display | | profileUrl? | string | undefined | Profile URL to navigate to | | profilePictureUrl? | string | undefined | Profile picture URL | # React Hooks Source: https://docs.scute.io/quickstarts/react-hooks Using Scute's hooks for authentication in React For a more granular control over your authentication flow, you can use the `@scute/react-hooks` package. This package exposes the `useAuth` and `useScuteClient` hooks, which allow you to build your own custom UI while still leveraging Scute's authentication logic. Head over to the [example projects repo](https://github.com/scuteai/js/tree/main/examples) to see the hooks in action and check out the [type docs](https://scute-js-docs.netlify.app/) for more `scuteClient` methods. To get started, install our React SDK with your favorite package manager: ```sh title="Terminal" theme={null} npm install @scute/react-hooks ``` Add your credentials to your environment variable handler: ```sh theme={null} VITE_SCUTE_APP_ID="YOUR_SCUTE_PROJECT_ID" VITE_SCUTE_BASE_URL="YOUR_SCUTE_BASE_URL" ``` **NOTE**: If you are not using Vite, use "REACT\_APP" as your prefix for your environment variables. ### Initialize the Scute client First initialize the Scute client using the `createClient` method exposed by `@scute/react-hooks` package: ```js theme={null} // scute.js import { createClient } from "@scute/react-hooks"; export const scute = createClient({ appId: import.meta.env.VITE_SCUTE_APP_ID, baseUrl: import.meta.env.VITE_SCUTE_BASE_URL, }); ``` ### Wrap your React app with Scute AuthContextProvider To be able to use the `useScuteClient` and `useAuth` hooks, wrap your app inside the Scute `AuthContextProvider`: ```jsx theme={null} // App.jsx import { AuthContextProvider } from "@scute/react-hooks"; import { scute } from "./scute"; export default function App() { return ( {/* This is where the rest of your application lives */} ); } ``` ### Using the hooks Now you can use the `useAuth` and `useScuteClient` hooks in your components. ```jsx theme={null} // MyComponent.jsx import { useAuth, useScuteClient } from "@scute/react-hooks"; export const MyComponent = () => { const { session, user, signOut } = useAuth(); const scute = useScuteClient(); if (session.status === "loading") { return
Loading...
; } if (session.status === "unauthenticated") { return (
); } return (

Welcome, {user.email}

); }; ``` ### `useAuth` The `useAuth` hook provides access to the authentication state and the current user. | Property | Type | Description | | --------- | ------------------------- | ----------------------------------------------------------------- | | `session` | `Session` | The current authentication session. | | `user` | `ScuteUserData` or `null` | The currently authenticated user, or `null` if not authenticated. | | `signOut` | `() => Promise` | A function to sign the user out. | ### `useScuteClient` The `useScuteClient` hook provides access to the Scute client instance. | Property | Type | Description | | -------- | ------------- | -------------------------- | | `scute` | `ScuteClient` | The Scute client instance. | # React Native / Expo Source: https://docs.scute.io/quickstarts/react-native # Scute React Native Integration Guide ## Installation First, install the required packages. The `@scute/react-hooks` package provides React hooks for authentication, while `react-native-webview` is needed for OAuth flows that require opening external authentication pages within your app. ```bash theme={null} npm install @scute/react-hooks react-native-webview ``` For Expo projects, you might also need to install the WebView package through Expo's CLI to ensure proper native module linking: ```bash theme={null} expo install react-native-webview ``` ## Setup ### 1. Initialize Scute Client Create a client instance in `scute.ts`. This client will handle all authentication operations and API calls to your Scute backend. The configuration requires your app ID and base URL, which should be stored as environment variables for security. ```typescript theme={null} import { createClient } from "@scute/react-hooks"; export const scuteClient = createClient({ appId: process.env.EXPO_PUBLIC_SCUTE_APP_ID!, baseUrl: process.env.EXPO_PUBLIC_SCUTE_BASE_URL!, }); ``` ### 2. Wrap Your App with AuthProvider In your root layout component (`_layout.tsx`), wrap your entire app with the `AuthContextProvider`. This provider makes the authentication state and client instance available to all components in your app through React Context, allowing you to access user data and authentication methods anywhere in your component tree. ```typescript theme={null} import { AuthContextProvider } from "@scute/react-hooks"; import { scuteClient } from "@/scute"; export default function RootLayout() { return ( ); } ``` ## Complete Login Form Implementation ### State Management Set up the necessary state variables to manage the authentication flow. The `identifier` stores the user's email or phone number, `otp` holds the verification code, `showOtpForm` controls which form is displayed, and `oAuthUrl` manages the OAuth WebView state. The `SCUTE_MAGIC_PARAM` is used to detect successful OAuth callbacks. ```typescript theme={null} import { useState } from "react"; import { useScuteClient, SCUTE_MAGIC_PARAM } from "@scute/react-hooks"; import { WebView } from "react-native-webview"; export default function LoginScreen() { const scuteClient = useScuteClient(); // Form states const [identifier, setIdentifier] = useState(""); const [otp, setOtp] = useState(""); const [showOtpForm, setShowOtpForm] = useState(false); const [oAuthUrl, setOAuthUrl] = useState(null); } ``` ### Email/Phone Login Form This is the initial login form that users see when they're not authenticated. It provides two authentication options: email/phone with OTP verification, and Google OAuth. The form only renders when neither the OTP form nor OAuth WebView is active. When users enter their identifier and tap "Sign in", it sends an OTP to their email or phone and transitions to the verification form. ```typescript theme={null} // Initial login form { !showOtpForm && !oAuthUrl && ( { await scuteClient.sendLoginOtp(identifier); setShowOtpForm(true); }} > Sign in / Sign up { const url = scuteClient.getOAuthUrl("google"); setOAuthUrl(url); }} > Sign in with Google ); } ``` ### OTP Verification Form This form appears after users request an OTP and allows them to enter the verification code they received. The numeric keyboard type makes it easier for users to input the code. When verification succeeds, the app signs in the user with the returned authentication payload and navigates to the profile screen. The back button allows users to return to the initial login form if needed. ```typescript theme={null} // OTP verification form { showOtpForm && ( { const { data, error } = await scuteClient.verifyOtp(otp, identifier); if (error) { console.log(error); } else { scuteClient.signInWithTokenPayload(data.authPayload); setShowOtpForm(false); router.push("/profile"); } }} > Verify setShowOtpForm(false)} > Back ); } ``` ## OAuth WebView Implementation ### WebView Configuration Configure platform-specific user agents to ensure OAuth providers recognize your app as a legitimate mobile browser. Different OAuth providers may have different requirements or behaviors based on the user agent string, so using platform-appropriate values helps avoid authentication issues and improves compatibility. ```typescript theme={null} import { Platform } from "react-native"; // User agent configuration for better compatibility const userAgent = Platform.select({ android: "Mozilla/5.0 (Linux; Android 10; Android SDK built for x86) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36", ios: "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1", }); ``` ### Complete WebView Component This WebView component handles the OAuth authentication flow by loading the OAuth provider's authentication page. It monitors URL changes to detect when the OAuth flow completes and returns to your app with an authorization code. The component includes comprehensive error handling, cookie management for session persistence, and proper security settings. When the OAuth callback is detected (via the magic parameter), it verifies the token and signs in the user automatically. ```typescript theme={null} { oAuthUrl && ( { const { nativeEvent } = syntheticEvent; console.warn("WebView error: ", nativeEvent); }} onHttpError={(event) => { console.log("HTTP error", event); }} onMessage={(event) => { console.log("Message from webview:", event.nativeEvent.data); }} onNavigationStateChange={async (event) => { const url = new URL(event.url); const code = url.searchParams.get(SCUTE_MAGIC_PARAM); if (!code) { return; } const { data, error } = await scuteClient.verifyMagicLinkToken( code ); if (error) { console.log(error); } else { scuteClient.signInWithTokenPayload(data.authPayload); setOAuthUrl(""); router.push("/profile"); } }} /> setOAuthUrl(null)} > Back ); } ``` ### Key WebView Properties Explained | Property | Purpose | | -------------------------- | ----------------------------------------------------- | | `sharedCookiesEnabled` | Enables cookie sharing between WebView and native app | | `thirdPartyCookiesEnabled` | Allows third-party cookies (required for OAuth) | | `domStorageEnabled` | Enables localStorage and sessionStorage | | `originWhitelist={["*"]}` | Allows navigation to any URL | | `userAgent` | Sets custom user agent for better compatibility | | `onNavigationStateChange` | Monitors URL changes to detect OAuth callback | ## Authentication State Management ### Using Authentication Hooks Use the `useAuth` hook to access the current authentication state and user information throughout your app. This hook provides reactive updates when the authentication state changes, automatically re-rendering components when users sign in or out. The example shows how to implement route protection by checking the session status and redirecting unauthenticated users to the login screen. ```typescript theme={null} import { useAuth, useScuteClient } from "@scute/react-hooks"; function ProfileScreen() { const { session, user } = useAuth(); const scuteClient = useScuteClient(); // Check authentication status if (session.status === "unauthenticated") { return ( You need to log in first to view your profile router.push("/")}> Go to Login ); } return ( scuteClient.signOut()} > Sign Out {JSON.stringify(user, null, 2)} ); } ``` ## Environment Configuration Create a `.env` file in your project root to store your Scute configuration securely. The `EXPO_PUBLIC_` prefix makes these variables available in your React Native code while keeping them separate from your source code. Replace the placeholder values with your actual Scute app ID and the URL of your Scute backend instance. ```env theme={null} EXPO_PUBLIC_SCUTE_APP_ID=your_app_id_here EXPO_PUBLIC_SCUTE_BASE_URL=https://your-scute-instance.com ``` ## Important Notes 1. **WebView Security**: The WebView configuration includes security settings for OAuth flows 2. **URL Monitoring**: `onNavigationStateChange` detects the OAuth callback with the magic parameter 3. **Error Handling**: Implement proper error handling for network issues and authentication failures 4. **User Agent**: Custom user agents improve compatibility with OAuth providers 5. **State Management**: Use React state to manage form visibility and authentication flow This implementation provides a complete authentication system with both email/phone OTP and OAuth support using React Native WebView. # React Router (Declarative) Source: https://docs.scute.io/quickstarts/react-router-declarative Using Scute with React Router for declarative routing For applications that need declarative routing with protected routes, you can combine Scute's React hooks with React Router. This approach provides a clean separation of concerns where each authentication step is handled as a separate route, and protected routes automatically redirect unauthenticated users. Head over to the [example projects repo](https://github.com/scuteai/js/tree/main/examples) to see the React Router implementation in action and check out the [type docs](https://scute-js-docs.netlify.app/) for more `scuteClient` methods. To get started, install our React SDK and React Router with your favorite package manager: ```sh title="Terminal" theme={null} npm install @scute/react-hooks react-router ``` Add your credentials to your environment variable handler: ```sh theme={null} VITE_SCUTE_APP_ID="YOUR_SCUTE_PROJECT_ID" VITE_SCUTE_BASE_URL="YOUR_SCUTE_BASE_URL" ``` **NOTE**: If you are not using Vite, use "REACT\_APP" as your prefix for your environment variables. ### Initialize the Scute client First initialize the Scute client using the `createClient` method exposed by `@scute/react-hooks` package: ```jsx theme={null} // providers.jsx import { createClient, AuthContextProvider } from "@scute/react-hooks"; const scuteClient = createClient({ appId: import.meta.env.VITE_SCUTE_APP_ID, baseUrl: import.meta.env.VITE_SCUTE_BASE_URL, }); export default function Providers({ children }) { return ( {children} ); } ``` ### Wrap your React app with providers and router Set up your app with both the Scute provider and React Router: ```jsx theme={null} // main.jsx import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import App from "./App.jsx"; import Providers from "./providers.jsx"; createRoot(document.getElementById("root")).render( ); ``` ### Set up your routes Define your routes declaratively with React Router: ```jsx theme={null} // App.jsx import { Routes, Route, Navigate } from "react-router-dom"; import { ProtectedRoute } from "./components/ProtectedRoute"; import { Login } from "./components/Login"; import { Profile } from "./components/Profile"; // ... other component imports function App() { return ( } /> } /> } /> } /> } /> } /> ); } ``` ### Create route components Each authentication step becomes its own component. Here's an example login component: ```jsx theme={null} // components/Login.jsx import { useScuteClient } from "@scute/react-hooks"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; export function Login() { const [identifier, setIdentifier] = useState(""); const scuteClient = useScuteClient(); const navigate = useNavigate(); const handleSubmit = async (e) => { e.preventDefault(); const { data, error } = await scuteClient.signInOrUp(identifier); if (error) { console.error("Sign in error:", error); return; } if (!data) { // Passkey verified, go to profile navigate("/profile"); } else { // Navigate to appropriate verification step if (identifier.includes("@")) { navigate("/magic-sent", { state: { identifier } }); } else { navigate("/otp-verify", { state: { identifier } }); } } }; return (

Sign In

setIdentifier(e.target.value)} />
); } ``` And a protected profile component: ```jsx theme={null} // components/Profile.jsx import { useAuth, useScuteClient } from "@scute/react-hooks"; import { useNavigate } from "react-router-dom"; export function Profile() { const { user } = useAuth(); const scuteClient = useScuteClient(); const navigate = useNavigate(); const handleSignOut = async () => { await scuteClient.signOut(); navigate("/login"); }; return (

Welcome, {user?.email}

{JSON.stringify(user, null, 2)}
); } ``` ### Route navigation patterns Use React Router's navigation hooks to move between authentication steps: ```jsx theme={null} // Navigate programmatically const navigate = useNavigate(); navigate("/profile"); // Navigate with state navigate("/otp-verify", { state: { identifier } }); // Access state in destination component const location = useLocation(); const { identifier } = location.state || {}; ``` # Svelte Source: https://docs.scute.io/quickstarts/svelte Using Scute with Svelte If you would like to integrate Scute authentication with your Svelte app, you can do so using the `@scute/js-core` package. This approach gives you full control over the authentication flow and allows you to build custom UI components that match your app's design. Head over to the [example project repo](https://github.com/scuteai/js/tree/main/examples/with-svelte) to check out the complete Svelte example and see how all the components work together. To get started, install the Scute core SDK with your favorite package manager: ```sh title="Terminal" theme={null} npm install @scute/js-core ``` Add your [credentials](/docs) to your environment variable handler: ```sh theme={null} VITE_SCUTE_APP_ID="YOUR_SCUTE_PROJECT_ID" VITE_SCUTE_BASE_URL="YOUR_SCUTE_BASE_URL" ``` ### Initialize the Scute client First initialize the Scute client using the `createClient` method exposed by `@scute/js-core` package: ```ts theme={null} // scute.ts import { createClient } from "@scute/js-core"; export const scuteClient = createClient({ appId: import.meta.env.VITE_SCUTE_APP_ID, baseUrl: import.meta.env.VITE_SCUTE_BASE_URL, }); ``` ### Create your main App component Create your main `App.svelte` component that handles the authentication flow: ```svelte theme={null}
{#if component === "profile"} {:else if component === "login"} {:else if component === "magic_verify"} {:else if component === "magic_sent"} {:else if component === "register_device"} {:else if component === "otp_verify"} {/if}
``` ### Create a Login component Create a `LoginForm.svelte` component for handling user authentication: ```svelte theme={null} ``` ### Create a Profile component Create a `Profile.svelte` component for authenticated users: ```svelte theme={null} {#if user}

Welcome!

User ID: {user.id}

Email: {user.email || 'N/A'}

Phone: {user.phoneNumber || 'N/A'}

{:else}
Loading...
{/if} ``` ### Additional Components The example also includes additional components for handling different authentication flows: * **`MagicVerify.svelte`** - For verifying magic link tokens * **`MagicSent.svelte`** - Shows confirmation when magic link is sent * **`OtpForm.svelte`** - For OTP verification (SMS/phone) * **`RegisterDevice.svelte`** - For WebAuthn device registration You can find the complete implementation of these components in the [example repository](https://github.com/scuteai/js/tree/main/examples/with-svelte/src/lib). ### Running the Example 1. Clone the example repository 2. Copy `env.example` to `.env` and add your credentials 3. Install dependencies: `npm install` 4. Start the development server: `npm run dev` Congrats! You now have a working Scute authentication system in your Svelte app! ## Core Methods The `scuteClient` provides several methods for authentication: ### `signInOrUp(identifier: string)` Initiates the sign-in or sign-up process with an email or phone number. ### `getSession()` Retrieves the current user session and user data. ### `signOut()` Signs out the current user and clears the session. ### `getMagicLinkToken()` Extracts magic link tokens from the current URL for verification. ### `verifyMagicLink(token: string)` Verifies a magic link token and completes authentication. For more methods and detailed API documentation, check out the [type docs](https://scute-js-docs.netlify.app/). # Vue.js Source: https://docs.scute.io/quickstarts/vue-js # Integrating Scute with Vue.js This guide will walk you through the process of integrating Scute into your Vue.js application, with specific focus on authentication features. ## Installation First, install the required Scute packages: ```bash theme={null} npm install @scute/core @scute/vue @scute/ui-vue # or yarn add @scute/core @scute/vue ``` ## Basic Setup ### 1. Initialize the Scute Client \\ # Verification Source: https://docs.scute.io/verification/index Verify users via email or SMS # Verification Verify a user's identity by sending a code or magic link to their email or phone. Use it for login, approvals, identity checks, or any action that needs confirmation. ## Methods * **OTP** — send a numeric code, user enters it * **Magic link** — send a link, user clicks it ## Channels Email and SMS. ## Flows **[Intent Verification](/guides/intent-verification)** — server-to-server. Your backend sends a verification request with context (ticket ID, intent name, contact info). User receives an SMS or email. Result comes back via webhook with a signed JWT. **[Verification SDK](/guides/verification-sdk)** — client-side. Any app using `@scute/js-core` can list, approve, and deny verifications. Works in browser, Electron, React Native. **[Self-serve](/verification/self-serve)** — send a magic link to verify an email address. User clicks the link, done. ## Modes Set in **Settings > Verification**. * **Dismiss** — confirm identity, show success, redirect. Default. * **Consent** — confirm identity, then ask approve or deny. Decision in webhook. ## Risk scoring Every completed verification includes a risk score: ```json theme={null} { "risk": { "score": 35, "level": "medium", "signals": ["new_ip", "phone_country_mismatch"], "recommendation": "review" } } ``` Returned in webhook payloads, API responses, and the `scute_token` JWT. # OTP Verification Source: https://docs.scute.io/verification/otp-verification # OTP Verification :::info OTP Verification can be initiated from the dashboard or programmatically via API. For dashboard-initiated flows, users must already exist in your Scute app. For API flows, you can use the identifier parameter to create users automatically. ::: ## OTP Verification Types OTP verification supports multiple channels: * **SMS OTP**: Traditional SMS-based verification to user's phone number * **User Identifier OTP**: Verification when changing user email/phone numbers (via email or SMS) ## Dashboard-Initiated OTP Flow 1. **Initiating Verification**: OTP verification is primarily designed for dashboard (operator) initiated flows. After entering a reason and clicking the `Send OTP` button, the system sends an SMS message to the user's registered phone number. Verification modal 2. **User Notification**: When the VerificationRequest is created, the user receives an OTP code via SMS and the verification request status is set to "pending". OTP verification dashboard 3. **Verification Process**: This flow is designed for scenarios where the operator is in direct communication with the user (typically on the phone). The user reads the OTP code they received, and the operator enters this code into the verification field in the dashboard. 4. **Completion**: Once the correct code is entered, the verification is completed, the status changes to "verified", and the operator can proceed with the authorized action. ## API-Initiated OTP Flow For programmatic verification flows, you can create OTP verification requests via API: ```bash theme={null} POST https://api.scute.io/v1/verify/:app_id/verifications ``` ### Authentication Requires M2M (Machine-to-Machine) authentication: ```bash theme={null} # Get M2M token first curl -X POST "https://api.scute.io/v1/auth/m2m/token" \ -H "Content-Type: application/json" \ -d '{"api_key": "your_api_key"}' ``` ### SMS OTP Example ```javascript theme={null} const response = await fetch('https://api.scute.io/v1/verify/your-app-id/verifications', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Authorization': 'your_m2m_access_token' }, body: JSON.stringify({ identifier: '+1234567890', channel: 'sms', verification_type: 'standard', reason: 'Phone number verification' }) }); const data = await response.json(); console.log(data.verification_id); // Track verification status ``` ### Supported OTP Types: * **SMS OTP**: Traditional SMS-based verification to phone numbers * **User identifier verification**: Change email/phone with OTP verification See [User Identifier Verification](./user-identifier-verification.mdx) for detailed examples of identifier change flows. # Email Verification Source: https://docs.scute.io/verification/self-serve Verify email addresses with magic links # Email Verification (Self-serve) Send a magic link to verify a user's email address. User clicks the link, done. ## Setup 1. Create an app in the [dashboard](https://control.scute.io) 2. Get an M2M token from **Settings > M2M Tokens** ## Send verification ```bash theme={null} curl -X POST "https://api.scute.io/v1/verify/{app_id}/verifications/intent" \ -H "Content-Type: application/json" \ -H "X-Authorization: Bearer {m2m_token}" \ -d '{ "intent_name": "Email Verification", "method": "email", "verification_type": "magic_link", "meta_data": { "contact_email": "user@example.com", "contact_name": "Jane" } }' ``` Response: ```json theme={null} { "verification_id": "uuid", "status": 200, "message": "Verification needed, sent to an email ending in user@example.com" } ``` ## What happens 1. User receives an email with a magic link 2. User clicks the link, lands on the verification page 3. Identity confirmed, status updates to `verified` 4. Webhook fires to your endpoint (if configured) ## Check status ```bash theme={null} curl "https://api.scute.io/v1/verify/{app_id}/verifications/{verification_id}" \ -H "X-Authorization: Bearer {m2m_token}" ``` ## Webhook Configure in **Settings > Webhooks**. You'll get: ```json theme={null} { "event_type": "verification.email.verified", "data": { "verification_id": "uuid", "meta_data": { "contact_email": "user@example.com" } } } ``` See [Webhooks Guide](/guides/webhooks) for setup and signature verification. # User Identifier Verification Source: https://docs.scute.io/verification/user-identifier-verification # User Identifier Verification User identifier verification allows you to securely change a user's email address or phone number after verifying ownership of the new identifier. This is essential for maintaining account security when users need to update their contact information. ## Overview The user identifier verification process involves: 1. **Creating a verification request** for the new email or phone number 2. **Sending an OTP code** to the new identifier 3. **User verifies ownership** by entering the OTP code 4. **Automatic update** of the user's identifier upon successful verification ## Supported Identifier Types * **Email addresses**: Users can change their registered email address * **Phone numbers**: Users can change their registered phone number ## Security Features ### Duplicate Prevention The system prevents duplicate identifiers within the same workspace: * No two users in the same workspace can have the same email address * No two users in the same workspace can have the same phone number * Users in different workspaces can have the same identifier ### Verification Required * Users must verify ownership of the new identifier before the change takes effect * OTP codes are sent directly to the new identifier * The old identifier remains active until verification is complete ## Creating User Identifier Verifications ### API Method Send a POST request to create a user identifier verification: ```bash theme={null} POST https://api.scute.io/v1/verify/:app_id/verifications ``` #### Authentication This endpoint requires M2M (Machine-to-Machine) authentication: ```bash theme={null} # Get M2M token first curl -X POST "https://api.scute.io/v1/auth/m2m/token" \ -H "Content-Type: application/json" \ -d '{"api_key": "your_api_key"}' ``` #### Request Parameters | Parameter | Type | Required | Description | | -------------------------- | ------ | -------- | ------------------------------------------------------ | | `identifier` | string | Yes | The new email address or phone number to verify | | `channel` | string | Yes | Must be "user\_identifier" for identifier changes | | `verification_type` | string | Yes | Must be "user\_identifier" for identifier verification | | `reason` | string | No | A description of why the identifier is being changed | | `metadata` | object | Yes | Contains user ID and identifier type information | | `metadata.user_id` | string | Yes | The ID of the user whose identifier will be changed | | `metadata.identifier_type` | string | Yes | Must be either "email" or "phone" | #### Example Request ```javascript theme={null} // Change user's email address const response = await fetch('https://api.scute.io/v1/verify/your-app-id/verifications', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Authorization': 'your_m2m_access_token' }, body: JSON.stringify({ identifier: 'newemail@example.com', channel: 'user_identifier', verification_type: 'user_identifier', reason: 'User requested email change', metadata: { user_id: 'user-uuid-here', identifier_type: 'email' } }) }); const data = await response.json(); console.log(data.verification_id); // Use this to track verification status ``` ```javascript theme={null} // Change user's phone number const response = await fetch('https://api.scute.io/v1/verify/your-app-id/verifications', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Authorization': 'your_m2m_access_token' }, body: JSON.stringify({ identifier: '+1234567890', channel: 'user_identifier', verification_type: 'user_identifier', reason: 'User requested phone change', metadata: { user_id: 'user-uuid-here', identifier_type: 'phone' } }) }); ``` #### cURL Example ```bash theme={null} # Change user's email address curl -X POST "https://api.scute.io/v1/verify/your-app-id/verifications" \ -H "Content-Type: application/json" \ -H "X-Authorization: your_m2m_access_token" \ -d '{ "identifier": "newemail@example.com", "channel": "user_identifier", "verification_type": "user_identifier", "reason": "User requested email change", "metadata": { "user_id": "user-uuid-here", "identifier_type": "email" } }' ``` #### Response The API returns a verification request object with: * `verification_id`: Unique identifier for tracking the verification * `status`: Initially set to "pending" * `channel`: Set to "user\_identifier" * `metadata`: Contains the new identifier and type information ## Verification Process ### 1. OTP Delivery After creating the verification request: * An OTP code is sent to the new identifier * For email: OTP is sent via email * For phone: OTP is sent via SMS (when SMS is configured) ### 2. Code Verification Users verify the OTP code using: ```bash theme={null} POST https://api.scute.io/v1/verify/:app_id/verifications/verify ``` ```javascript theme={null} const response = await fetch('https://api.scute.io/v1/verify/your-app-id/verifications/verify', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Authorization': 'your_m2m_access_token' }, body: JSON.stringify({ verification_id: 'verification-uuid-here', code: '123456' }) }); const result = await response.json(); if (result.verified) { console.log('Identifier successfully changed!'); } ``` ### 3. Automatic Update Upon successful verification: * The user's identifier is automatically updated in the database * The confirmation timestamp is set (`email_confirmed_at` or `phone_confirmed_at`) * The verification status changes to "verified" ## Error Handling The API returns specific error codes for common scenarios: ### Duplicate Identifier ```json theme={null} { "error": "This email is already used by another user in this workspace", "error_code": "identifier_already_exists" } ``` ### Invalid Format ```json theme={null} { "error": "Invalid email format", "error_code": "invalid_email_format" } ``` ### Invalid Type ```json theme={null} { "error": "Identifier type must be email or phone", "error_code": "invalid_identifier_type" } ``` ## Best Practices ### 1. User Experience * Always inform users that they'll receive a verification code at their new identifier * Provide clear instructions about checking spam folders for email verifications * Set appropriate expectations for delivery time ### 2. Security * Validate identifier formats on the client side before sending requests * Handle duplicate identifier errors gracefully * Consider rate limiting verification requests to prevent abuse ### 3. Error Handling * Display specific error messages to help users understand what went wrong * Provide retry mechanisms for failed verifications * Log verification attempts for audit purposes ## Integration with Dashboard The Scute dashboard provides a user-friendly interface for identifier changes: * Users can initiate identifier changes through the profile settings * Real-time validation prevents duplicate identifiers * Automatic redirect to verification status page after request creation ## Monitoring and Analytics Track verification metrics through: * Verification request creation events * Success/failure rates by identifier type * Time-to-completion analytics * User adoption of identifier change features ## Webhooks Configure webhooks to receive notifications when: * Identifier verification requests are created * Verifications are completed successfully * Verifications fail or expire # Tracking Verification Status Source: https://docs.scute.io/verification/verification-status # Tracking Verification Status After creating a verification request, it's important to track its status to understand where it is in the verification lifecycle. This guide explains the possible statuses and how to check them. ## Verification Request Statuses A verification request can have one of the following statuses: | Status | Description | | ----------- | ---------------------------------------------------------------------------------- | | `pending` | The verification request has been created but not yet completed by the user | | `verified` | The user has successfully completed the verification process | | `failed` | The verification attempt was unsuccessful (e.g., wrong OTP entered too many times) | | `cancelled` | The verification request was cancelled by the system or an administrator | | `expired` | The verification request has expired (time limit exceeded) | ## Checking Verification Status You can check the status of a verification request using our API. ### API Endpoint ``` GET https://api.scute.io/v1/verify/:app_id/verifications/:id ``` #### Request Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------- | | `app_id` | string | Your Scute application ID (in the URL path) | | `id` | string | The ID of the verification request to check | #### Example Request ```javascript theme={null} // Using fetch const response = await fetch('https://api.scute.io/v1/verify/your-app-id/verifications/verification-id', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_SCUTE_API_KEY' } }); const data = await response.json(); console.log(data.status); // Will show one of: pending, verified, failed, cancelled, expired ``` ## Handling Different Statuses ### Pending When a verification is in the `pending` status: * The user has not yet completed the verification process * You may want to display a message to the user or send a reminder ### Verified When a verification reaches the `verified` status: * The user has successfully completed verification * Your application can proceed with the protected action * This is the successful end state of the verification flow ### Failed When a verification has the `failed` status: * The verification attempt was unsuccessful * You may want to prompt the user to try again with a new verification request ### Cancelled When a verification is `cancelled`: * The verification request was stopped prematurely * This typically happens through admin action or system decision ### Expired When a verification has `expired`: * The verification request has exceeded its time limit * You should prompt the user to start a new verification process