# Participant sessions

> Connect your app’s signed-in user to an AdQuest participant without exposing your private secret.

Source: https://dashboard.adquest.batriders.ir/docs/participant-sessions · Audience: Developers · Section: Build

## Prerequisites

- A platform that is accepted and active.
- Your Secret Key (aqs_), stored on your server.
- Users who are already signed in to your app.

## How identity flows

1. The user signs in to your application using your existing authentication.
2. Your frontend calls an authenticated endpoint on your own backend.
3. Your backend reads the user ID from the verified session and calls AdQuest with the Secret Key.
4. Return the participant token and expiry to that user’s frontend. Initialize the SDK with the same external user ID.

> **Choose a stable identity.** Use an opaque, stable ID from your own user system. Do not accept an arbitrary external_user_id from an unauthenticated request, use a shared demo ID for everyone, or use a phone number or email when an internal ID will do.

## Create a participant token

**Node / Next.js backend helper**

```typescript
// Server only. Call this AFTER verifying your application's login session.
export async function createParticipantSession(user: { id: string; name?: string }, locale?: string) {
  const baseUrl = process.env.ADQUEST_API_URL;
  const secret = process.env.ADQUEST_PLATFORM_SECRET;
  if (!baseUrl || !secret) throw new Error("AdQuest is not configured");

  const response = await fetch(baseUrl.replace(/[/]$/, "") + "/platform/participant-sessions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-AdQuest-Platform-Secret": secret,
    },
    body: JSON.stringify({ external_user_id: user.id, display_name: user.name, locale }),
    cache: "no-store",
  });
  const result = await response.json();
  if (!response.ok || !result.status || !result.data?.participant_token) {
    throw new Error("Could not create participant session");
  }
  return {
    externalUserId: user.id,
    participantToken: result.data.participant_token,
    expiresAt: result.data.expires_at,
    expiresIn: result.data.expires_in,
  };
}
```

Expose the helper as your own authenticated route, such as POST /api/adquest/session. Your route must:

- Return 401 when your session lookup fails.
- Protect cookie-based requests against CSRF.
- Send Cache-Control: no-store.
- Rate-limit session creation.
- Never return the Secret Key or log the participant token.

## Language and country

Language is set in two places that must match: your backend sends locale when it creates the participant session, and the widget or embed gets the same locale for its own text. Your frontend picks the language and passes it to your backend. The browser SDK never chooses a language, creates sessions or sends the Secret Key.

| Setting | Rule |
| --- | --- |
| Supported values | English (en) and Persian (fa), with region tags such as en-US. Missing, invalid or unsupported values use English. Persian switches the interface to right-to-left. |
| Widget or embed text and direction | The locale you pass to the widget or embed. If omitted, English. Nothing is read from the browser or from a platform setting. |
| Which quests and lottery text load | The locale in the participant session request, stored with the session. The response data.locale_resolution shows { requested, effective, source }, where source is host (your locale was used) or legacy_en (English was used). Send the same value you give the widget. |
| When the user changes language | Create a new participant session with the new locale, then update the widget or embed. |
| Reward country | Never taken from the language. Your backend sends country_code when it creates the session. AdQuest never derives it from locale, browser language, Accept-Language, timezone, IP address or host_context. |
| Browser language | Never read automatically. If you want it, call browserLocales() from @adquest/sdk, pick a supported value, and pass that value as your explicit locale to both the session request and the widget. The helper returns up to 8 browser languages, or an empty list on the server. |

- [React widget](https://dashboard.adquest.batriders.ir/docs/react#language): Pass the locale prop.
- [Hosted iframe & WebView](https://dashboard.adquest.batriders.ir/docs/embed#language): Pass hostContext.locale.

## Expiry, refresh and logout

1. Keep the token and expiresAt in memory for the active user. Refresh through your backend before expiry or when AdQuestWidget calls onAuthRequired.
2. Pass the refreshed value as participantToken. A headless client uses client.setParticipantToken(token); the bridge uses adquest:setParticipantToken.
3. On logout or user change, remove the widget / WebView and clear its participant state. Mint a new token and remount for the new user. Do not reuse the previous user’s quests or balance.

> **Handle failures visibly.** If session creation fails, show a retry or sign-in state. A blocked participant requires resolving the account issue; repeatedly refreshing the token will not unblock them.

## Profile and host context

**Optional host_context in the backend request**

```json
{
  "version": 1,
  "displayName": "Ada",
  "locale": "en-US",
  "timezone": "Asia/Tehran",
  "location": { "status": "unavailable" },
  "push": { "status": "denied" },
  "capabilities": {
    "profile": "granted", "location": "unavailable",
    "push": "denied", "custom": "unavailable"
  }
}
```

Add host_context for profile quests. Send only what the user allowed.

- Supported fields: display_name, username, email, phone, avatar_url, locale and timezone.
- location, push and capabilities use granted, denied or unavailable.
- Keep host_context at or below 8 KB.
- Save profile changes in your app, then create a new participant session. A frontend-only change does not prove a profile requirement.

## Winner contact

| Flow | What you send | What AdQuest does |
| --- | --- | --- |
| Email on every session | The participant session includes email and display name. | After the draw, AdQuest emails the stored address. |
| Email after the win | The session sends only external_user_id. After lottery.winner, your backend posts the winner email to POST /platform/lottery-claims/{claim}/contact. | AdQuest emails the address you post. |
| Claim in the app | The session sends only external_user_id, and you do not post an email. | The winner submits the claim form in the widget. AdQuest does not send a win email. |

> **One mailer.** There is no contact setting. AdQuest sends the win email when the session included an address, including an address sent for a profile quest, and when your backend posts the winner email after the draw. Do not send that email yourself. Do not post contact for anyone who is not the winner of that claim. A lottery.winner webhook is separate. Its payload does not include email or phone.

## Next steps

- [React & Next.js](https://dashboard.adquest.batriders.ir/docs/react): Show the widget with the participant token.
- [Hosted iframe & WebView](https://dashboard.adquest.batriders.ir/docs/embed): Pass the token through the bridge.
- [Participant sessions API](https://dashboard.adquest.batriders.ir/docs/api/participant-sessions): The full request and response.
