Skip to main content
Emblem Developer Docs
Emblem Connect (Partner Integration)

Enrollment Flow

Create an Emblem pass after your provider verification succeeds.

What Enrollment Is

Enrollment lets a user create an Emblem pass after your provider has already verified them. The user opts in, completes passkey creation on Emblem, and can skip verification next time through assertion.

A user who already has an Emblem pass (for example one that returned requires_verification with LEVEL_UPGRADE_REQUIRED because it held only L1) signs in with that pass on the enrollment page instead. Your verification is added to their existing pass: they keep one pass, your assertions keep the same subject for them, and their next sign-in satisfies the new level. You start enrollment and handle the callback exactly as for a new pass.

When To Use It

Use enrollment after:

  • Your own verification flow completed successfully.
  • You have the verification facts you need.
  • The user chooses to create an Emblem pass.

Enrollment is not automatic. It is an explicit post-verification opt-in flow.

Prerequisites

You need:

  • A provisioned client_id
  • A registered redirect_uri
  • A secret API key (emb_sk_...)

The same client_id used for assertion can be used for enrollment, but it must also be provisioned with credential issuance authority.

End-to-End Flow

Your verification completes successfully
User opts in to "Create an Emblem pass"
Your server  →  POST /api/v1/credentials/enroll/start  →  receives enrollment_url
Browser      →  redirects to Emblem                    →  user creates passkey
Browser      →  returns to your redirect_uri           →  with status or error

Start Enrollment

POST /api/v1/credentials/enroll/start records verification provenance and creates the enrollment session in one call.

import crypto from 'node:crypto'
import { createClient } from '@emblemapp/sdk'

const emblem = createClient({
  apiKey: process.env.EMBLEM_SECRET_KEY!,
})

const enrollment = await emblem.startEnrollment({
  client_id: process.env.EMBLEM_ASSERTION_CLIENT_ID!,
  redirect_uri: 'https://provider.example/emblem/enroll/callback',
  state: crypto.randomUUID(),
  provider: 'SAFEPASSAGE',
  verification_level: 'L1',
  external_verification_id: 'provider-verification-123',
  verified_at: new Date().toISOString(),
  external_user_ref: 'user-123',
  external_session_ref: 'journey-456',
})

res.redirect(enrollment.enrollment_url)

Request fields

FieldRequiredDescription
client_idYesYour provisioned authorization client
redirect_uriYesCallback URI to receive the final enrollment outcome
stateYesYour CSRF or correlation value
providerYesVerification provider enum, currently SAFEPASSAGE, PRIVATEAV, or EMBLEM_LEGACY
verification_levelYesVerification level satisfied by your verification: L1 or L2
external_verification_idYesImmutable provider-side verification identifier
verified_atYesISO-8601 timestamp for when your verification completed
external_user_refNoYour user reference
external_session_refNoYour journey or session reference

Response fields

FieldDescription
enrollment_urlRedirect target for the Emblem-hosted passkey flow
correlation_idTrace identifier for support and logs
expires_atSession expiry timestamp. Enrollment sessions last 10 minutes

One call records provenance and starts enrollment

Enrollment start is both the provenance-recording step and the browser-session creation step. There is no separate issuance API call in the Connect flow.

Callback Handling

Enrollment is terminal on the browser callback. There is no back-channel code exchange step.

Success callback

https://provider.example/emblem/enroll/callback?status=success&state=opaque_state

Failure callback

https://provider.example/emblem/enroll/callback?error=cancelled&error_code=USER_CANCELLED&state=opaque_state

Callback outcomes

Query paramsMeaning
status=success, stateThe user created a pass, or added this verification to the pass they already had
error, error_code, stateThe flow ended without successful enrollment

The callback can arrive more than once for the same enrollment. If the user reopens a completed enrollment link (for example with Back or refresh, or after the first redirect did not reach you), Emblem sends the success callback again, so treat status=success idempotently for the same state. An expired enrollment link always returns SESSION_EXPIRED to your redirect_uri, even when it is opened after the session ended.

Current callback error mappings

errorerror_codeMeaning
expiredSESSION_EXPIREDEnrollment session expired
already_usedSESSION_ALREADY_USEDThe link was completed earlier, but the pass it created no longer exists (for example after the user deleted their Emblem data)
unavailableCLIENT_UNAVAILABLEClient is unavailable for enrollment
cancelledUSER_CANCELLEDUser cancelled the flow
not_supportedPASSKEY_NOT_SUPPORTEDBrowser or platform does not support passkeys
already_registeredALREADY_REGISTEREDThe passkey already exists
registration_failedREGISTRATION_FAILEDPasskey registration failed
invalid_requestINVALID_REQUESTThe enrollment flow could not continue because the request was invalid

Always validate state on callback before treating the flow as complete.

Idempotency

Enrollment start is idempotent on external_verification_id within the same client_id.

  • If you retry with the same external_verification_id and the same immutable verification details, Emblem reuses the stored provenance and returns a fresh enrollment_url.
  • If you retry with conflicting verification details, Emblem returns 409 IDEMPOTENCY_CONFLICT.

Treat external_verification_id as immutable per completed verification. The immutable details are provider, verification_level, and verified_at. Send the time the verification actually completed as verified_at, not a value that changes when your own record is updated; otherwise a retry (for example after the user returns to your page) is refused with 409 IDEMPOTENCY_CONFLICT and the user cannot create a pass.

Error Reference

Start enrollment errors

CodeHTTP statusMeaningWhat to do
VALIDATION_ERROR400Request body failed validationCheck required fields, enums, and ISO-8601 timestamps
INVALID_REDIRECT_URI400redirect_uri is not registered for the client, or it already contains reserved enrollment callback paramsRegister the exact URI you send and exclude state, status, error, and error_code
UNAUTHORIZED401Missing or invalid secret keyCheck Authorization: Bearer emb_sk_...
PUBLISHER_SUSPENDED403Tenant is suspended by EmblemContact Emblem support or your account owner
CLIENT_INACTIVE403Authorization client is inactiveAsk Emblem to activate the client
ISSUANCE_NOT_ALLOWED403Client is not provisioned for credential issuanceAsk Emblem to enable enrollment authority for the client
NOT_FOUND404Authorization client not foundVerify client_id and environment alignment
RATE_LIMITED429Too many requestsBack off and honor Retry-After
IDEMPOTENCY_CONFLICT409The same external_verification_id was reused with conflicting detailsTreat the verification tuple as immutable
INTERNAL_ERROR500Emblem-side failureRetry if appropriate and log request_id / correlation_id
RATE_LIMITER_UNAVAILABLE503Emblem's rate-limit dependency is temporarily unavailable, so the request was refusedRetry after Retry-After; the user's verification on your side is unaffected

Notes

  • redirect_uri must be registered on the authorization client.
  • redirect_uri must not already include state, status, error, or error_code query params. Emblem owns those callback keys and will reject ambiguous enrollment callback URIs.
  • Connect endpoints use secret-key server auth only.
  • Enrollment requires separate issuance authority even if the same client is already approved for assertion.

On this page