# Build Custom User Verifier

> Build a custom user verifier server for Arcade.dev in any language. Use when implementing Arcade's user verification flow, building an /auth/verify endpoint, integrating Arcade tool authorization into a production app, or troubleshooting verification issues like missing user_id or session cookies during the OAuth callback.

- Skill: `arcadeai/build-custom-user-verifier` (Agent Skill)
- Install (CLI): `npx skillmds@latest add arcadeai/build-custom-user-verifier`
- Raw SKILL.md: https://api.skillmd.com/api/skills/arcadeai/build-custom-user-verifier/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: ArcadeAI (https://skillmd.com/u/arcadeai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/arcadeai/build-custom-user-verifier

---


# Build a Custom User Verifier for Arcade.dev

> This is a companion skill for the [Secure Auth in Production](https://docs.arcade.dev/en/guides/user-facing-agents/secure-auth-production) documentation page.

## How the verification flow works

When an app uses Arcade to authorize a tool on behalf of a user:

1. Your app calls `client.tools.authorize(tool_name=..., user_id="alice")` or sets the `Arcade-User-ID: alice` header on a gateway request
2. Arcade returns an authorization URL for the user to visit (e.g. Google OAuth consent)
3. After the user completes consent, Arcade **redirects the user's browser** (HTTP 303) to your custom verifier URL with a `flow_id` query parameter:
   ```
   GET https://your-app.com/auth/verify?flow_id=af_abc123
   ```
4. Your server identifies the user from **its own session** (cookie, token, etc.) — Arcade does NOT send any user identity
5. Your server calls the Arcade API to confirm: "the user on this browser session is `alice`"
6. Arcade checks that `alice` matches the `user_id` from step 1 — if so, verification succeeds

This prevents **OAuth phishing**: an attacker starting a flow with their own `user_id`, then tricking a victim into completing consent, which would give them the ability to execute actions on your user's upstream service account (Gmail, Notion, etc).

## Prerequisites

- An Arcade API key (set as `ARCADE_API_KEY` environment variable)
- Your app must have its own user session mechanism (cookies, JWTs, etc.)
- Users must be signed into your app before starting any Arcade tool authorization

## Build the verifier endpoint

You need one server-side route: a GET endpoint that handles the browser redirect from Arcade.

### Pseudocode

```
ROUTE GET /auth/verify:

    // 1. Get the flow_id from the query string (provided by Arcade)
    flow_id = request.query_params["flow_id"]
    IF flow_id is missing:
        RETURN error 400 "Missing flow_id parameter"

    // 2. Get the user's identity from YOUR app's session.
    //    Arcade does NOT send this. Your server must already know
    //    who the user is via session cookie, JWT, or similar.
    user_id = get_user_id_from_your_session(request)
    IF user_id is missing:
        RETURN error 401 "User is not signed in"

    // 3. Call Arcade's confirm_user API (server-side, using your API key)
    TRY:
        result = arcade_client.auth.confirm_user(
            flow_id = flow_id,
            user_id = user_id
        )
    CATCH error:
        RETURN error 400 "Verification failed"

    // 4. Wait for the auth flow to complete
    TRY:
        auth_response = arcade_client.auth.wait_for_completion(result.auth_id)
        IF auth_response.status == "completed":
            RETURN success page or redirect to your app
        ELSE:
            RETURN error page "Authorization was not completed"
    CATCH error:
        IF result.next_uri exists:
            REDIRECT to result.next_uri
        RETURN error 500 "Could not confirm authorization status"
```

### Python

```python
from arcadepy import Arcade

client = Arcade()  # uses ARCADE_API_KEY env var

# Inside your GET /auth/verify handler:
result = client.auth.confirm_user(flow_id=flow_id, user_id=user_id)
auth_response = client.auth.wait_for_completion(result.auth_id)
```

### JavaScript/TypeScript

```typescript
import { Arcade } from "@arcadeai/arcadejs";

const client = new Arcade(); // uses process.env.ARCADE_API_KEY

// Inside your GET /auth/verify handler:
const result = await client.auth.confirmUser({
  flow_id: flowId,
  user_id: userId,
});
const authResponse = await client.auth.waitForCompletion(result.auth_id);
```

### REST API (any language)

If your language doesn't have an Arcade SDK, call the REST API directly:

```
POST https://cloud.arcade.dev/api/v1/oauth/confirm_user
Authorization: Bearer <your_arcade_api_key>
Content-Type: application/json

{
  "flow_id": "<flow_id from the query string>",
  "user_id": "<user_id from your app session>"
}
```

Success response (200):
```json
{
  "auth_id": "ac_2zKml...",
  "next_uri": "https://..."
}
```

Error response (400) — `user_id` does not match the one that started the flow:
```json
{
  "code": 400,
  "msg": "An error occurred during verification"
}
```

## Critical requirements

1. **The `user_id` must match on both sides.** The ID passed to `confirm_user` must be identical to the one your app used when starting the authorization flow (`client.tools.authorize(user_id=...)` or the `Arcade-User-ID` header).

2. **The user must already be signed into your app.** Arcade does a browser redirect, so the browser sends cookies for your domain — but only if they already exist. No session means no user identity at verification time.

3. **The `confirm_user` call must be server-side.** It uses your Arcade API key, which must never be exposed to the client.

4. **Your verifier URL must be publicly reachable.** Arcade redirects the user's browser to it. For local development, use a tunnel like [ngrok](https://ngrok.com/).

## Register in the Arcade Dashboard

1. Go to [Auth > Settings](https://api.arcade.dev/dashboard/auth/settings)
2. Select **Custom verifier**
3. Enter the full URL of your endpoint (e.g. `https://your-app.com/auth/verify`)

## Common mistakes

| Mistake | Symptom | Fix |
|---|---|---|
| User not signed into your app before the auth flow | `user_id` is missing from session at verification time | Ensure users authenticate with your app first |
| Different `user_id` on start vs. verify | Arcade returns 400 on `confirm_user` | Use the same identifier in both places |
| Calling `confirm_user` from the browser | API key exposed in client-side code | Move the call to your server |
| Verifier URL not publicly accessible | Arcade redirect fails or times out | Use ngrok or deploy to a public host |
| Not handling `wait_for_completion` | Verification appears to hang | Poll or await `wait_for_completion`, fall back to `next_uri` |

## Reference implementation

The [custom-user-verifier](https://github.com/ArcadeAI/custom-user-verifier) repo is a complete working example in Python/Flask. Key file: `app.py`, function `verify_user()`. Clone it and read the source to understand the full pattern.

## Further reading

- [Secure Auth in Production](https://docs.arcade.dev/en/guides/user-facing-agents/secure-auth-production) — full Arcade documentation
- [Arcade Python SDK](https://github.com/ArcadeAI/arcade-py)
- [Arcade JavaScript SDK](https://github.com/ArcadeAI/arcade-js)

