> ## Documentation Index
> Fetch the complete documentation index at: https://partners.docs.polymarket.us/llms.txt
> Use this file to discover all available pages before exploring further.

# Firms, participants and accounts

> You get two firms, not one. Where a participant ID comes from, where x-participant-id goes, and an account-scoped read end to end.

You get **two** firms, not one, and conflating them is the largest single source of doc-caused tickets we have. Read this section before you write your first account-scoped call.

<Info>
  **API reference:** <a href="https://docs.polymarket.us/institutional/accounts/overview" target="_blank" rel="noreferrer">Accounts</a>. Opens on the public documentation site in a new tab.
  Where it disagrees with this page, this page is authoritative for the partner surface.
</Info>

## Your two firms

<Note>
  **If you are an IB, this differs.**
  You get **three** identities, not two: the participant firm, the clearing-member firm, and an
  **IB anchor DCO account**, which is not the clearing-member firm. Record and label all three.
  Using a DCO clearing-member ID where a DCM firm name belongs stalls onboarding with no error
  returned to you. The physical-person legal-name rule also bites at the moment you record these
  strings — see [Naming and record requirements](/regulatory#naming-and-record-requirements).
</Note>

| Firm                     | Shape                                               | Role                                                                  |
| ------------------------ | --------------------------------------------------- | --------------------------------------------------------------------- |
| **Participant firm**     | `firms/20260821-examplefirminc-api-participant`     | Holds your end users. Every `x-participant-id` must be scoped here    |
| **Clearing-member firm** | `firms/20260821-examplefirminc-api-clearing-member` | The `firm_id` on your Auth0 token. Your account and clearing identity |

The two strings differ only in the suffix: `-api-participant` versus `-api-clearing-member`. We give you both strings at your credential gate — record both, and do not construct either. What the leading digits mean is not published; do not read a date, an ordering or anything else out of them.&#x20;

<Note>
  Legacy examples show three different prefix shapes — `firms/ISV-Participant-YourFirmID/...`,
  `firms/ISV-YourFirmID/...` and `firms/ISV-Example-ClearingMember/...` — with no explanation. None of
  them is the real shape. Use the two strings we gave you, verbatim.
</Note>

## `whoami` returns the wrong firm for this purpose

`GET /v1/whoami` resolves your **clearing-member** firm (`ep3_account_firm_name`), not your participant firm (`ep3_participant_firm_name`). It is the right call for "which firm is this token", and the wrong basis for addressing a user.

<Snippet file="participant-id-warning.mdx" />

Concretely, what happens when you build a participant ID from the `whoami` firm and send it:

1. The request fails `validateParticipantFirmOwnership`.
2. It raises `CROSS_ISV_PARTICIPANT_IMPERSONATION_ATTEMPT` — a **CRITICAL** security log entry.
3. It increments a cross-ISV access metric, against your firm, as a legitimate partner.

Three legacy pages instruct exactly this. If you followed one of them, tell us in your shared Slack channel so the alerts on your account can be attributed correctly.

## Where a participant ID actually comes from

**The authoritative sources are two, and both come from KYC:**

* `participantId` on the `kyc.approved` webhook, or
* `participantId` from `GET /v1/kyc/status` after a decision of `ACCEPT`.

Record it against your own user row at onboarding. It is the value you will send as `x-participant-id` for the life of that user.

<Warning>
  **`participantId` can come back empty on an `ACCEPT` while provisioning finishes.** Observed gaps
  run from \~500 ms up to **26 minutes**. Wait for the `kyc.approved` webhook before you enable
  trading for that user; do not poll `kyc/status` once and treat an empty `participantId` as a
  failure.
</Warning>

`GET /v1/users` does not solve this. It is a **roster read**: it is account-scoped and requires `x-participant-id` itself, so it cannot be used to discover your first participant ID. There is no bootstrap listing.

## One SSN is one account, platform-wide

**A trading account ID is derived from the end user's SSN, and one SSN maps to exactly one exchange account across the entire platform** — the same rule the Polymarket mobile app enforces.

<Warning>
  **Never share an SSN across test users.** Reusing a test SSN points every one of those users at a
  single account. Transfers then fail with `NOT_FOUND`, and unpicking it requires manual database
  cleanup by us. This caused a 66-reply outage thread.

  There is also **no re-KYC, reset or PII-update path**: a user whose SSN is already in use cannot be
  re-KYC'd by you.
</Warning>

Give every preprod test user a distinct SSN from your own fixture set, and record which user holds which.

## `provisionedAccount` is opaque

`provisionedAccount` is the fully-qualified DCM account name. **Its account identifier is opaque and cannot be derived from participant or user data.** Store the string we return; never assemble it.

Legacy examples showing `firms/.../accounts/user-123-trading` teach the wrong model — they imply the account segment is your user ID, and it is not.

<Warning>
  **`GET /v1/accounts?user=<participant>` returns `invalid user`** for the same participant string
  that works in `x-participant-id`.

  There is no published alternative. Capture `provisionedAccount` at onboarding, because you may not
  be able to look it up later.
</Warning>

## Where `x-participant-id` goes

| Call                                                | Header                                                                                                                                                                                      |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Account-scoped reads — positions, reports, accounts | **Required**                                                                                                                                                                                |
| Drop copy stream                                    | **Required** — it is account-scoped                                                                                                                                                         |
| Balance ledger stream                               | **Required** — it is per-account                                                                                                                                                            |
| `CreateVendorOrder`                                 | **Not used.** The participant is named only by `order.account`                                                                                                                              |
| `CashMovementService` (all three RPCs)              | **Must not be sent.** Cash movements are firm-scoped                                                                                                                                        |
| `/v1/orderbook/*`                                   | Not required                                                                                                                                                                                |
| Market data streaming                               | Not required                                                                                                                                                                                |
| `/v1/refdata/*`                                     | Not required                                                                                                                                                                                |
| `/v1/health`                                        | Not required — but \*\*\*\*, and it is not a working health check: partners report **404 in production** and **401 in preprod** against it. See [Environments and endpoints](/environments) |
| `CreateInstrumentStateChangeSubscription`           | Not required — `read:instruments` only                                                                                                                                                      |
| `/v1/report/trades/stats`                           | See the note below                                                                                                                                                                          |

<Warning>
  **Whether `/v1/report/trades/stats` needs the header is unresolved.**

  The legacy troubleshooting page lists it under endpoints that do **not** require
  `x-participant-id`, and a partner hit `PERMISSION_DENIED` on exactly that call. Do not build a
  retry that flips the header blindly; if you hit it, report the verbatim error and we will settle
  it.
</Warning>

## An account-scoped read, end to end

Each snippet imports the canonical token client from [Authentication](/authentication).

<CodeGroup>
  ```python users.py theme={null}
  # Canonical token client from the "Authentication" section
  from polymarket_auth import PolymarketClient

  import requests

  # From the `participantId` on the kyc.approved webhook for this user (see "Firms, participants and accounts").
  # Never constructed by hand, never derived from GET /v1/whoami.
  PARTICIPANT_ID = "REPLACE_WITH_PARTICIPANT_ID_FROM_WEBHOOK"

  client = PolymarketClient()

  resp = requests.get(
      f"{client.api_base_url}/v1/users",
      headers={
          "authorization": f"Bearer {client.access_token()}",
          "x-participant-id": PARTICIPANT_ID,  # required: /v1/users is account-scoped
      },
      timeout=10,
  )
  resp.raise_for_status()  # 403 here almost always means a missing or wrong header
  print(resp.json())
  ```

  ```typescript users.ts theme={null}
  // Canonical token client from the "Authentication" section
  import { PolymarketClient } from "./polymarketAuth";

  // From the `participantId` on the kyc.approved webhook for this user (see "Firms, participants and accounts").
  const PARTICIPANT_ID = "REPLACE_WITH_PARTICIPANT_ID_FROM_WEBHOOK";
  const API_BASE_URL = "https://api.preprod.polymarketexchange.com"; // see "Environments and endpoints"

  const client = new PolymarketClient();

  const res = await fetch(`${API_BASE_URL}/v1/users`, {
    headers: {
      authorization: `Bearer ${await client.accessToken()}`,
      "x-participant-id": PARTICIPANT_ID, // required: /v1/users is account-scoped
    },
  });
  if (!res.ok) throw new Error(`GET /v1/users ${res.status}: ${await res.text()}`);
  console.log(await res.json());
  ```

  ```bash users.sh theme={null}
  #!/usr/bin/env bash
  set -euo pipefail

  # ACCESS_TOKEN comes from token.sh in "Authentication".
  ACCESS_TOKEN="${ACCESS_TOKEN:?run token.sh from the Authentication section first}"
  # From the `participantId` on the kyc.approved webhook for this user.
  PARTICIPANT_ID="REPLACE_WITH_PARTICIPANT_ID_FROM_WEBHOOK"
  API_BASE_URL="https://api.preprod.polymarketexchange.com"   # see "Environments and endpoints"

  curl -sS --fail-with-body "${API_BASE_URL}/v1/users" \
    -H "authorization: Bearer ${ACCESS_TOKEN}" \
    -H "x-participant-id: ${PARTICIPANT_ID}"
  ```

  ```go users.go theme={null}
  package main

  import (
  	"fmt"
  	"io"
  	"net/http"

  	// Canonical token client from the "Authentication" section
  	"example.com/yourfirm/pmauth"
  )

  // From the `participantId` on the kyc.approved webhook for this user (see "Firms, participants and accounts").
  const ParticipantID = "REPLACE_WITH_PARTICIPANT_ID_FROM_WEBHOOK"

  func main() {
  	client := pmauth.New()
  	tok, err := client.AccessToken()
  	if err != nil {
  		panic(err)
  	}

  	req, _ := http.NewRequest(http.MethodGet, pmauth.APIBaseURL+"/v1/users", nil)
  	req.Header.Set("authorization", "Bearer "+tok)
  	req.Header.Set("x-participant-id", ParticipantID) // required: /v1/users is account-scoped

  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	defer resp.Body.Close()
  	body, _ := io.ReadAll(resp.Body)
  	if resp.StatusCode != http.StatusOK {
  		panic(fmt.Sprintf("GET /v1/users %d: %s", resp.StatusCode, body))
  	}
  	fmt.Println(string(body))
  }
  ```
</CodeGroup>

<Warning>
  **The response shape of `GET /v1/users` is not yet published here.**

  Log the raw body on your first call and branch only on fields you have seen.
</Warning>

For gRPC, the same value goes in metadata alongside `authorization`. Note the field-naming split you will hit across surfaces: KYC requests are `snake_case`, REST responses are `camelCase`, and webhooks are `snake_case`.

## What can go wrong

| Where                            | Symptom                                                                   | Cause                                                                    | What you do                                                                          |
| -------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| Firms, participants and accounts | `CROSS_ISV_PARTICIPANT_IMPERSONATION_ATTEMPT` in a ticket about your firm | A participant ID built from the `whoami` firm                            | Use the `participantId` from `kyc.approved`, unmodified; tell us the alert was yours |
| Firms, participants and accounts | `403` on positions, reports, drop copy or balance ledger                  | `x-participant-id` missing                                               | Add the header; see [Where `x-participant-id` goes](#where-x-participant-id-goes)    |
| Firms, participants and accounts | `PERMISSION_DENIED` on a cash movement                                    | `x-participant-id` sent on a firm-scoped call                            | Remove the header                                                                    |
| Firms, participants and accounts | Empty `participantId` after `ACCEPT`                                      | Provisioning still running — up to 26 minutes observed                   | Wait for the `kyc.approved` webhook                                                  |
| Firms, participants and accounts | `NOT_FOUND` on transfers for several users at once                        | A test SSN shared across those users, so they all resolve to one account | Stop; it needs manual cleanup by us. Raise it in Slack                               |
| Firms, participants and accounts | `invalid user` from `GET /v1/accounts?user=<participant>`                 | Known gap; the parameter does not accept a participant ID                | Use the `provisionedAccount` you captured at onboarding                              |
| Firms, participants and accounts | You cannot find a participant ID for an existing user                     | `GET /v1/users` requires one itself                                      | Re-read your KYC records; there is no bootstrap listing                              |

## Next

[Limits and errors](/limits-and-errors)
