Account Linking Integration
Connect your players' accounts to Oncade so you can personalize rewards, enable campaign payouts, and confirm user identity during server-side operations. This guide walks through creating link sessions, presenting the hosted link flow, and reacting to status updates.
Server-to-server flow for a stable user reference
- From your server, call
POST /api/v1/users/link/initiatewith an optional player email (orGET /api/v1/users/link/initiatewithout email) using your Server API key and game ID headers. If email is not provided, the player will sign in or sign up during the linking flow. Store the returnedsessionKeyand the hostedurl. - Redirect the player to the hosted
urlto complete the account link. Keep thesessionKeyassociated with that player in your own records. - (Optional) Poll
GET /api/v1/users/link/details?session=<sessionKey>to track progress while the player is in the flow. - Listen for the
User.Account.Link.Succeededwebhook. When it arrives, save the provideduserRefalongside thesessionKeyin your database—thisuserRefis the stable identifier you will reuse for future API calls on behalf of that player. - Handle
User.Account.Link.Canceled,User.Account.Link.Failed, andUser.Account.Link.Removedwebhooks to keep your records accurate. Remove or retry any sessions that do not complete and treat removed links as unlinked users.
With the userRef stored, you can pass it to other server APIs (for example, campaign event ingestion) as the durable way to reference that player without exposing personal information.
Prerequisites
- Oncade developer account with access to the game you are integrating.
- Server API key and game ID from DevPortal → Games.
- Secure environment variables for
ONCADE_SERVER_API_KEY,ONCADE_GAME_ID,ONCADE_API_BASE_URL, andONCADE_WEBHOOK_SECRET. - HTTPS endpoint that can receive webhooks. See the Webhook Integration guide for signature verification details.
1. Create a linking session
When a player requests to link their account, call the POST /api/v1/users/link/initiate endpoint from your server with an optional email parameter, or use GET /api/v1/users/link/initiate to create a session without email. If email is not provided, the player will sign in or sign up during the linking flow. Include an Idempotency-Key header for safe retries.
import { NextRequest, NextResponse } from 'next/server';
import { v4 as uuidv4 } from 'uuid';
type InitiateBody = {
email?: string; // Optional - if not provided, email will be resolved at sign-in/sign-up time
};
type InitiateResponse = {
url: string;
sessionKey: string;
};
export async function POST(request: NextRequest) {
const body: InitiateBody = await request.json();
const { email } = body;
const res = await fetch(`${process.env.ONCADE_API_BASE_URL}/v1/users/link/initiate`, {
method: 'POST',
headers: {
Authorization: `Bearer undefined`,
'X-Game-Id': process.env.ONCADE_GAME_ID!,
'X-Oncade-API-Version': 'v1',
'Content-Type': 'application/json',
'Idempotency-Key': uuidv4(),
},
body: JSON.stringify(email ? { email } : {}),
});
if (!res.ok) {
const error = await res.json();
return NextResponse.json({ error: error.error }, { status: res.status });
}
const data: InitiateResponse = await res.json();
return NextResponse.json(data);
}The API response includes a hosted url for your player and a sessionKey you will reuse in subsequent steps.
curl -s -X POST \
-H "Authorization: Bearer $SERVER_KEY" \
-H "X-Game-Id: $GAME_ID" \
-H "X-Oncade-API-Version: v1" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: link-$(uuidgen)" \
-d '{"email":"player@example.com"}' \
https://<host>/api/v1/users/link/initiateExample response
// HTTP 201 Created (new session) or 200 OK (existing pending session)
{
"url": "https://oncade.gg/link?session=session_a1b2c3d4e5f6...",
"sessionKey": "session_a1b2c3d4e5f6..."
}2. Send the player to the hosted flow
- Persist the
sessionKeyreturned by the initiate call. - Redirect or open a new window to the
urlprovided in the response. - Show an in-game confirmation state while the player completes the hosted steps.
3. Poll session status (optional)
If you need real-time feedback (for example, to unblock gameplay), poll GET /api/v1/users/link/details with the session key. The response includes status fields such as state, userRef, and timestamps for each step of the flow.
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const session = searchParams.get('session');
if (!session) {
return NextResponse.json({ error: 'Session key is required' }, { status: 400 });
}
const res = await fetch(`${process.env.ONCADE_API_BASE_URL}/v1/users/link/details?session=${session}`, {
headers: {
Authorization: `Bearer ${process.env.ONCADE_SERVER_API_KEY}`,
'X-Game-Id': process.env.ONCADE_GAME_ID!,
'X-Oncade-API-Version': 'v1',
},
});
if (!res.ok) {
const error = await res.json();
return NextResponse.json({ error: error.error }, { status: res.status });
}
return NextResponse.json(await res.json());
}Example responses
// HTTP 200 OK - Pending (player has not yet approved)
{
"namespaceType": "game",
"gameId": "game_abc123",
"gameName": "My Game",
"gameImage": "https://example.com/game-logo.png",
"prefilledEmail": "player@example.com",
"environment": "production"
}The userRef field only appears once the player has approved the link.
4. Handle webhook events
Webhooks deliver final state transitions and the persistent userRef you need to recognize the player in future API calls. Configure your endpoint in DevPortal → Games → Webhooks and verify the signature on every request.
import crypto from 'crypto';
import { NextRequest, NextResponse } from 'next/server';
function verifySignature(body: string, signature: string, secret: string) {
const expected = crypto.createHmac('sha256', secret).update(body).digest('hex');
if (signature.length !== expected.length || signature.length % 2 !== 0) {
return false;
}
let provided: Buffer;
let expectedBuffer: Buffer;
try {
provided = Buffer.from(signature, 'hex');
expectedBuffer = Buffer.from(expected, 'hex');
} catch {
return false;
}
if (provided.length !== expectedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(provided, expectedBuffer);
}
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get('x-oncade-signature');
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 401 });
}
if (!verifySignature(body, signature, process.env.ONCADE_WEBHOOK_SECRET!)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const payload = JSON.parse(body);
switch (payload.event) {
case 'User.Account.Link.Succeeded':
await markLinked(payload.data.sessionKey, payload.data.userRef);
break;
case 'User.Account.Link.Canceled':
await releasePendingSession(payload.data.sessionKey);
break;
case 'User.Account.Link.Removed':
await disconnect(payload.data.sessionKey, payload.data.userRef);
break;
default:
console.warn('Unhandled webhook event', payload.event);
}
return NextResponse.json({ received: true });
}Example webhook payload
// Webhook: User.Account.Link.Succeeded
{
"event": "User.Account.Link.Succeeded",
"data": {
"sessionKey": "session_a1b2c3d4e5f6...",
"user_ref": "usr_xyz789...",
"metadata": {
"idempotencyKey": "link-abc123-def456"
}
}
}Common events you will receive:
User.Account.Link.Started— the player opened the hosted flow.User.Account.Link.Succeeded— the player approved the link and you can storeuserRef.User.Account.Link.Canceled— the player backed out before finishing.User.Account.Link.Failed— something prevented completion. Use the message field for debugging.User.Account.Link.Removed— the link was later revoked and the player should be treated as unlinked.
5. Recommended player experience
- Show a loading state while the initiate endpoint responds.
- Confirm to the player that they will complete linking in a secure browser window.
- After receiving a success webhook, refresh any cached profile or campaign eligibility data.
- If the session times out or a cancellation webhook arrives, allow the player to retry with a new initiate call.
Test the flow end-to-end
- Use a staging game and webhook secret to avoid affecting live players.
- Trigger
POST /api/v1/users/link/initiatewith a test email, or useGET /api/v1/users/link/initiateto test the flow without email. - Complete the hosted steps and confirm that
User.Account.Link.Succeededarrives at your webhook endpoint. - Call
GET /api/v1/users/link/details?session=<sessionKey>to verify the final state now includesuserRef. - From DevPortal, remove the linked account and observe the
User.Account.Link.Removedwebhook.
Next steps
- Use the returned
userRefwhen submitting campaign events (Campaigns Integration). - Subscribe to the full list of account events via Webhook Integration.
- Review Subscription guides if you plan to gate recurring benefits behind linked accounts.