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

Assertion Flow

Check whether a user already has an Emblem pass that satisfies your policy.

What Assertion Is

The assertion flow checks whether a user already has an Emblem pass that satisfies your verification policy. If reusable proof exists, your server can exchange an authorization code for a slim assertion payload. If it does not, Emblem returns a normal "continue with your own verification" outcome.

When To Use It

Use assertion when you want an "Already verified with Emblem?" branch inside your product before running your own verification flow.

Typical pattern:

  • Offer Emblem as an optional shortcut.
  • Check for reusable proof first.
  • Fall back to your normal verification when reusable proof is not available.

Prerequisites

You need:

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

Use redirect_uri consistently for both transaction creation and code exchange.

End-to-End Flow

Your server  →  POST /api/v1/assertions/transactions   →  receives authorize_url
Browser      →  redirects or opens popup to Emblem     →  user authenticates
Emblem       →  checks for reusable proof              →  evaluates policy
Browser      →  returns to your redirect_uri           →  with code or error
Your server  →  POST /api/v1/assertions/token          →  receives assertion data

Browser Callback Contract

The browser callback to your redirect_uri has three meaningful outcomes:

OutcomeQuery paramsMeaning
Successcode, stateReusable proof was approved and you can exchange the code on your server
No reusable prooferror=requires_verification, error_code=NO_REUSABLE_RECORD, stateThe user authenticated to Emblem but does not have reusable proof that satisfies your policy
Level upgrade needederror=requires_verification, error_code=LEVEL_UPGRADE_REQUIRED, stateThe user has proof, but not at the requested level

requires_verification is not a transport failure. It is the normal branch where you continue with your own verification flow.

Always validate state before taking action on the callback.

Redirect Pattern

Use full-page redirect when you are comfortable handing the whole browser window to Emblem during the check.

1. Create a transaction on your server

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

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

const transaction = await emblem.createAssertionTransaction({
  client_id: process.env.EMBLEM_ASSERTION_CLIENT_ID!,
  redirect_uri: 'https://provider.example/emblem/callback',
  state: crypto.randomUUID(),
  nonce: crypto.randomUUID(),
  required_level: 'L1',
})

2. Redirect the browser

res.redirect(transaction.authorize_url)

3. Handle the callback

app.get('/emblem/callback', async (req, res) => {
  if (req.query.state !== req.session.assertionState) {
    return res.status(403).send('Invalid state')
  }

  if (req.query.error === 'requires_verification') {
    return res.redirect('/verify-with-provider')
  }

  const assertion = await emblem.exchangeAssertionCode({
    grant_type: 'authorization_code',
    code: req.query.code as string,
    client_id: process.env.EMBLEM_ASSERTION_CLIENT_ID!,
    redirect_uri: 'https://provider.example/emblem/callback',
  })

  // Use assertion.assertion.subject, level, and verified_at in your app.
  res.json(assertion)
})

Use popup mode when you want to keep your current page loaded and let Emblem run in a separate window.

Parent page

import { openAssertionPopup } from '@emblemapp/sdk'

const transaction = await emblem.createAssertionTransaction({
  client_id: process.env.EMBLEM_ASSERTION_CLIENT_ID!,
  redirect_uri: 'https://provider.example/emblem/popup-callback',
  state: crypto.randomUUID(),
  nonce: crypto.randomUUID(),
  required_level: 'L1',
})

const result = await openAssertionPopup(transaction.authorize_url, {
  expectedOrigin: window.location.origin,
  timeoutMs: 120000,
})

if (result.status === 'success') {
  await fetch('/api/emblem/assertion/exchange', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code: result.code, state: result.state }),
  })
} else if (result.status === 'error') {
  if (result.error === 'requires_verification') {
    window.location.href = '/verify-with-provider'
  }
} else if (result.status === 'closed') {
  // Popup closed before a callback reached the opener.
  // Reconcile with the stored transaction_id or offer retry.
}

Host a small callback page on your own origin and relay the result back to the opener:

import { handleAssertionPopupCallback } from '@emblemapp/sdk'

handleAssertionPopupCallback({
  targetOrigin: 'https://provider.example',
  expectedState: sessionStorage.getItem('emblem_assertion_state') ?? undefined,
})

openAssertionPopup() resolves to one of:

  • { status: 'success', code, state }
  • { status: 'error', error, errorCode, state }
  • { status: 'closed' }

handleAssertionPopupCallback() normalizes the query string and posts the result to window.opener. If autoClose is not disabled, it closes the popup after delivery.

Caller-owned callback page

Your popup callback page is hosted by you, not by Emblem. Emblem redirects the popup back to your redirect_uri, and that page calls handleAssertionPopupCallback().

Server-Side Code Exchange

Exchange approved authorization codes on your server with POST /api/v1/assertions/token.

const result = await emblem.exchangeAssertionCode({
  grant_type: 'authorization_code',
  code: req.body.code,
  client_id: process.env.EMBLEM_ASSERTION_CLIENT_ID!,
  redirect_uri: 'https://provider.example/emblem/callback',
})

Request parameters

ParameterRequiredDescription
grant_typeYesMust be authorization_code
codeYesThe browser-returned authorization code
client_idYesYour provisioned authorization client
redirect_uriNoIf provided, must exactly match the redirect_uri used on transaction creation

Response shape

{
  "transaction_id": "emb_atx_123",
  "correlation_id": "corr_123",
  "assertion": {
    "client_id": "emb_cli_123",
    "subject": "emb_sub_pairwise_value",
    "transaction_id": "emb_atx_123",
    "nonce": "nonce_123",
    "level": "L1",
    "verified_at": "2026-03-24T20:00:00.000Z",
    "authenticated_at": "2026-03-24T20:01:00.000Z",
    "source_provider": "SAFEPASSAGE"
  }
}

Assertion payload field guide

FieldMeaning
client_idYour authorization client
subjectStable pairwise user identifier for this client. The same user gets the same subject for the same client_id, but a different one for a different client
levelVerification level satisfied by the reusable proof: L1 or L2
verified_atWhen the original verification happened
authenticated_atWhen the user authenticated to Emblem during this assertion flow
source_providerProvider/provenance enum for the reusable proof. EMBLEM_LEGACY represents imported legacy proof rather than a live provider verification
nonceYour original transaction nonce, echoed back

Codes are single-use and expire after 2 minutes.

Reconciliation And Interrupted Flows

Use GET /api/v1/assertions/transactions/{transactionId} when the browser flow did not cleanly complete on your side.

Typical cases:

  • The user closes the popup.
  • The browser is interrupted before your callback finishes.
  • The transaction expires while the user is still on Emblem.
  • Your app needs to distinguish pending from approved from requires_verification.

Always store transaction_id when you create the transaction.

What code_available means

If reconciliation returns status: APPROVED and code_available: true, an unconsumed authorization code still exists for that transaction.

It does not return the code itself. You still need the front-channel callback to obtain it.

Status action model

StatusWhat it meansRecommended action
PENDINGUser has not completed the flow yetKeep waiting briefly or let the user retry
AUTHENTICATEDUser authenticated to Emblem, but evaluation is not completeKeep waiting briefly or let the user retry
APPROVEDReusable proof satisfied policyIf code_available=true, wait for callback or restart if your UX timed out
REQUIRES_VERIFICATIONNo reusable proof or level upgrade requiredContinue your normal verification flow
EXCHANGEDCode was already exchanged on some server pathTreat the flow as already completed
FAILED or CANCELLEDThe flow ended without approvalShow a recoverable error and offer retry
EXPIREDTransaction TTL elapsedStart a fresh transaction

Error Reference

Transaction creation

CodeHTTP statusMeaningWhat to do
VALIDATION_ERROR400Missing or invalid request fieldsCheck client_id, redirect_uri, state, and enum values
INVALID_REDIRECT_URI400redirect_uri is not registered for the clientRegister the exact URI you send
UNAUTHORIZED401Missing or invalid secret keyCheck Authorization: Bearer emb_sk_...
PUBLISHER_SUSPENDED403Tenant is suspended by EmblemContact Emblem support or your account owner
CLIENT_INACTIVE403Authorization client exists but is inactiveAsk Emblem to activate the client
NOT_FOUND404Authorization client not foundVerify client_id and environment alignment
RATE_LIMITED429Too many requestsBack off and honor Retry-After
INTERNAL_ERROR500Emblem-side failureRetry if appropriate and log request_id / correlation_id

Token exchange

CodeHTTP statusMeaningWhat to do
VALIDATION_ERROR400Request body failed validationCheck grant_type, code, and client_id
INVALID_GRANT400Exchange failed for an unmatched grant reasonTreat as a terminal exchange failure and restart
EXPIRED_CODE400Authorization code expiredStart a fresh assertion transaction
REUSED_CODE400Authorization code was already consumedDo not retry the same code
CODE_NOT_FOUND400Code does not existConfirm you are exchanging the latest callback code
CLIENT_MISMATCH400Code does not belong to the provided client_idUse the same client_id from transaction creation
REDIRECT_URI_MISMATCH400Provided redirect_uri does not match the original transactionSend the exact same redirect_uri, or omit it if you do not need the equality check
UNAUTHORIZED401Missing or invalid secret keyCheck the Authorization header
CLIENT_INACTIVE403Authorization client is inactiveAsk Emblem to activate the client
NOT_FOUND404Authorization client not foundVerify client_id and environment
TRANSACTION_EXPIRED409Transaction expired before exchangeRestart the flow
TRANSACTION_NOT_APPROVED409Transaction completed without reusable proof approvalFollow the browser callback outcome or reconciliation result
TRANSACTION_NOT_READY409Transaction is not yet ready to exchangeWait briefly or restart if the user abandoned the flow
RATE_LIMITED429Too many requestsBack off and honor Retry-After
INTERNAL_ERROR500Emblem-side failureRetry if appropriate and log trace ids

Reconciliation

CodeHTTP statusMeaningWhat to do
VALIDATION_ERROR400Invalid transaction IDSend an emb_atx_... id
UNAUTHORIZED401Missing or invalid secret keyCheck your Authorization header
NOT_FOUND404Transaction not found for this publisherVerify transaction_id and environment alignment
RATE_LIMITED429Too many lookup requestsBack off and honor Retry-After
INTERNAL_ERROR500Emblem-side failureRetry if needed and log trace ids

Constraints

  • Redirect and popup are supported. Iframe embedding is not.
  • Connect uses secret-key client auth only. PKCE and public clients are not supported.
  • Assertion does not auto-escalate into hosted verification. If reusable proof is not available, your app must explicitly continue its own verification flow.

On this page