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 errorStart 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
| Field | Required | Description |
|---|---|---|
client_id | Yes | Your provisioned authorization client |
redirect_uri | Yes | Callback URI to receive the final enrollment outcome |
state | Yes | Your CSRF or correlation value |
provider | Yes | Verification provider enum, currently SAFEPASSAGE, PRIVATEAV, or EMBLEM_LEGACY |
verification_level | Yes | Verification level satisfied by your verification: L1 or L2 |
external_verification_id | Yes | Immutable provider-side verification identifier |
verified_at | Yes | ISO-8601 timestamp for when your verification completed |
external_user_ref | No | Your user reference |
external_session_ref | No | Your journey or session reference |
Response fields
| Field | Description |
|---|---|
enrollment_url | Redirect target for the Emblem-hosted passkey flow |
correlation_id | Trace identifier for support and logs |
expires_at | Session 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_stateFailure callback
https://provider.example/emblem/enroll/callback?error=cancelled&error_code=USER_CANCELLED&state=opaque_stateCallback outcomes
| Query params | Meaning |
|---|---|
status=success, state | The user created a pass, or added this verification to the pass they already had |
error, error_code, state | The 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
error | error_code | Meaning |
|---|---|---|
expired | SESSION_EXPIRED | Enrollment session expired |
already_used | SESSION_ALREADY_USED | The link was completed earlier, but the pass it created no longer exists (for example after the user deleted their Emblem data) |
unavailable | CLIENT_UNAVAILABLE | Client is unavailable for enrollment |
cancelled | USER_CANCELLED | User cancelled the flow |
not_supported | PASSKEY_NOT_SUPPORTED | Browser or platform does not support passkeys |
already_registered | ALREADY_REGISTERED | The passkey already exists |
registration_failed | REGISTRATION_FAILED | Passkey registration failed |
invalid_request | INVALID_REQUEST | The 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_idand the same immutable verification details, Emblem reuses the stored provenance and returns a freshenrollment_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
| Code | HTTP status | Meaning | What to do |
|---|---|---|---|
VALIDATION_ERROR | 400 | Request body failed validation | Check required fields, enums, and ISO-8601 timestamps |
INVALID_REDIRECT_URI | 400 | redirect_uri is not registered for the client, or it already contains reserved enrollment callback params | Register the exact URI you send and exclude state, status, error, and error_code |
UNAUTHORIZED | 401 | Missing or invalid secret key | Check Authorization: Bearer emb_sk_... |
PUBLISHER_SUSPENDED | 403 | Tenant is suspended by Emblem | Contact Emblem support or your account owner |
CLIENT_INACTIVE | 403 | Authorization client is inactive | Ask Emblem to activate the client |
ISSUANCE_NOT_ALLOWED | 403 | Client is not provisioned for credential issuance | Ask Emblem to enable enrollment authority for the client |
NOT_FOUND | 404 | Authorization client not found | Verify client_id and environment alignment |
RATE_LIMITED | 429 | Too many requests | Back off and honor Retry-After |
IDEMPOTENCY_CONFLICT | 409 | The same external_verification_id was reused with conflicting details | Treat the verification tuple as immutable |
INTERNAL_ERROR | 500 | Emblem-side failure | Retry if appropriate and log request_id / correlation_id |
RATE_LIMITER_UNAVAILABLE | 503 | Emblem's rate-limit dependency is temporarily unavailable, so the request was refused | Retry after Retry-After; the user's verification on your side is unaffected |
Notes
redirect_urimust be registered on the authorization client.redirect_urimust not already includestate,status,error, orerror_codequery 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.