Skip to main content
After a user completes (or exits) the consent flow, they’re redirected to your redirect_uri with query parameters indicating the result.

Callback parameters

ParameterAlways PresentDescription
statusYessuccess, reauthorized, or failure
stateYesThe state value you provided in the link
uidYesThe user identifier returned by Emerge. If you provided uid, you get the same value; if you omitted it, Emerge generates a pairwise uid.
error_codeOnly on failureError code explaining the failure
Do not ask end-users for uid or collect it in the frontend. Store the callback uid server-side and map it to your internal user record. This avoids wrong-user data access and keeps identifiers private.

Status values

success

First-time consent granted. This user has connected their data for the first time with your application.
https://yourapp.com/callback?status=success&state=abc123&uid=psub_d4e5f6789012345678901234abcdef01

reauthorized

User re-linked an existing consent. This happens when:
  • User clicks the link again after previously consenting
  • User re-authenticates after token expiration
https://yourapp.com/callback?status=reauthorized&state=abc123&uid=psub_d4e5f6789012345678901234abcdef01

failure

User cancelled or an error occurred during the flow.
https://yourapp.com/callback?status=failure&state=abc123&uid=psub_d4e5f6789012345678901234abcdef01&error_code=user_failed

Error codes

Error CodeDescriptionSuggested Action
user_failedUser cancelled or denied consentShow retry option
data_invalidInsufficient data in user’s accountExplain data requirements
access_deniedUser denied OAuth permissionsExplain why permissions are needed
invalid_scopeRequired scopes not availableContact support
admin_policy_enforcedOrganization policy blocks accessUser needs admin approval
uid_conflictDifferent user already linked with this uidUse a unique uid per user

Handling callbacks

import express from 'express';

const app = express();

app.get('/emerge/callback', async (req, res) => {
  const { status, state, uid, error_code } = req.query;

  // 1. Verify state matches (CSRF protection)
  const expectedState = req.session.emergeState;
  if (state !== expectedState) {
    console.error('State mismatch - possible CSRF attack');
    return res.status(400).redirect('/error?reason=invalid_state');
  }

  // Clear stored state
  delete req.session.emergeState;

  // 2. Handle based on status
  switch (status) {
    case 'success':
      // First-time consent - data export will start automatically
      await saveUserConsent(uid as string, {
        consentedAt: new Date()
      });

      // Redirect to success page
      return res.redirect('/dashboard?connected=true');

    case 'reauthorized':
      // Existing consent refreshed
      await updateUserConsent(uid as string, {
        reauthorizedAt: new Date()
      });

      return res.redirect('/dashboard?reconnected=true');

    case 'failure':
      // Handle specific error codes
      const errorMessage = getErrorMessage(error_code as string);

      return res.redirect(`/connect?error=${encodeURIComponent(errorMessage)}`);

    default:
      return res.status(400).redirect('/error');
  }
});

function getErrorMessage(errorCode: string): string {
  const messages: Record<string, string> = {
    'user_failed': 'You cancelled the connection. Click below to try again.',
    'data_invalid': 'Your account doesn\'t have enough data history yet.',
    'access_denied': 'Permission was denied. We need access to provide this feature.',
    'admin_policy_enforced': 'Your organization\'s policy prevents this connection.'
  };

  return messages[errorCode] || 'Something went wrong. Please try again.';
}

State verification

Always verify the state parameter matches what you stored before the redirect. This prevents CSRF attacks where an attacker tricks a user into linking their account to the attacker’s data.
StorageProsCons
SessionSimple, automatic cleanupRequires session middleware
RedisDistributed, TTL supportAdditional infrastructure
DatabasePersistent, auditableMore complex queries
Signed cookieStatelessSize limitations

What happens after success

When you receive success or reauthorized:
  1. Data export starts automatically - Emerge begins exporting the user’s data from their provider
  2. Export takes 1-15 minutes - Depending on data volume
  3. Webhook updates (optional) - You may receive consent events plus data.ready/data.failed for provider-level export updates
  4. Poll export readiness (recommended fallback) - Check GET /export/status/{uid} and query only when the provider in sources[] has data_ready: true
Use provider-level readiness from sources[] instead of assuming all providers are ready at once.

Completed session handling

If a user clicks a consent link after they’ve already completed consent:
  • They see a brief “already connected” message
  • They’re redirected to the Data Wallet (not back to your redirect_uri)
  • No new callback is triggered
This prevents confusion from re-processing completed flows.