After a user completes (or exits) the consent flow, they’re redirected to your redirect_uri with query parameters indicating the result.
Callback parameters
| Parameter | Always Present | Description |
|---|
status | Yes | success, reauthorized, or failure |
state | Yes | The state value you provided in the link |
uid | Yes | The user identifier returned by Emerge. If you provided uid, you get the same value; if you omitted it, Emerge generates a pairwise uid. |
error_code | Only on failure | Error 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 Code | Description | Suggested Action |
|---|
user_failed | User cancelled or denied consent | Show retry option |
data_invalid | Insufficient data in user’s account | Explain data requirements |
access_denied | User denied OAuth permissions | Explain why permissions are needed |
invalid_scope | Required scopes not available | Contact support |
admin_policy_enforced | Organization policy blocks access | User needs admin approval |
uid_conflict | Different user already linked with this uid | Use 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.
Recommended state storage
| Storage | Pros | Cons |
|---|
| Session | Simple, automatic cleanup | Requires session middleware |
| Redis | Distributed, TTL support | Additional infrastructure |
| Database | Persistent, auditable | More complex queries |
| Signed cookie | Stateless | Size limitations |
What happens after success
When you receive success or reauthorized:
- Data export starts automatically - Emerge begins exporting the user’s data from their provider
- Export takes 1-15 minutes - Depending on data volume
- Webhook updates (optional) - You may receive consent events plus
data.ready/data.failed for provider-level export updates
- 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.