Skip to main content
Email OTP (One-Time Passcode) provides a passwordless authentication method where users receive a 6-digit code via email. OTPs are valid for 10 minutes and can only be used once.

Implement Email OTP

The Discovery flow is designed for centralized login pages where users authenticate before selecting which Organization to access.
1

Send the OTP

Send a one-time passcode to the user’s email address:
curl --request POST \
  --url https://test.stytch.com/v1/b2b/otps/email/discovery/send \
  --header 'Content-Type: application/json' \
  --user 'PROJECT_ID:SECRET' \
  --data '{
    "email_address": "user@example.com"
  }'
Parameters:
  • email_address: The user’s email address
  • discovery_expiration_minutes: (Optional) Expiration time in minutes (default: 10)
  • login_template_id: (Optional) Custom email template ID
  • locale: (Optional) Language for the email (e.g., “en”, “es”)
Response:
{
  "status_code": 200,
  "request_id": "request-id-test-b05c992f-ebdc-489d-a754-c7e70ba13141"
}
2

Authenticate the OTP

After the user receives and enters the code, authenticate it:
curl --request POST \
  --url https://test.stytch.com/v1/b2b/otps/email/discovery/authenticate \
  --header 'Content-Type: application/json' \
  --user 'PROJECT_ID:SECRET' \
  --data '{
    "email_address": "user@example.com",
    "code": "123456"
  }'
Parameters:
  • email_address: The user’s email address
  • code: The 6-digit OTP code
Response:
{
  "status_code": 200,
  "request_id": "request-id-test-b05c992f-ebdc-489d-a754-c7e70ba13141",
  "intermediate_session_token": "SeiGwdj5lKkrEVgcEY3QNJXt6srxS3IK2Nwkar6mXD4=",
  "email_address": "user@example.com",
  "discovered_organizations": [
    {
      "organization": {
        "organization_id": "organization-test-...",
        "organization_name": "Acme Corp",
        "organization_slug": "acme-corp"
      },
      "membership": {
        "type": "eligible_to_join_by_email_domain",
        "details": {}
      }
    }
  ]
}
3

Exchange the intermediate session

After the user selects an organization, exchange the intermediate session token for a full session:
curl --request POST \
  --url https://test.stytch.com/v1/b2b/discovery/intermediate_sessions/exchange \
  --header 'Content-Type: application/json' \
  --user 'PROJECT_ID:SECRET' \
  --data '{
    "intermediate_session_token": "SeiGwdj5lKkrEVgcEY3QNJXt6srxS3IK2Nwkar6mXD4=",
    "organization_id": "organization-test-...",
    "session_duration_minutes": 60
  }'
Response:
{
  "status_code": 200,
  "request_id": "request-id-test-...",
  "member_id": "member-test-...",
  "session_token": "mZAYn5aLEqKUlZ_Ad9U_fWr38GaAQ1oFAhT8ds245v7Q",
  "session_jwt": "eyJhbGc...",
  "member": { /* member object */ },
  "organization": { /* organization object */ },
  "session": { /* session object */ }
}
The Discovery flow returns an intermediate_session_token which must be exchanged for a full session after the user selects their organization.

Next steps