Skip to main content
BETA — SUBJECT TO CHANGE. This API is in beta and may change without notice.
KYC outcomes that resolve after the initial request — instant approvals (whose account provisioning completes asynchronously), DocV results, and manual-review decisions — are delivered to a webhook URL you register. Webhooks are how you learn a participant’s final provisioned provisioned_participant and any rejection.
Only terminal outcomes are delivered. Intermediate states (DocV in progress, manual review) produce no webhook — poll GET /v1/kyc/status if you need to track them.

Register your webhook

POST /v1/kyc/webhook — requires the write:kyc scope. Register once per firm (not per participant). Registration runs a test POST to your URL before saving; if the test fails, nothing is saved and you may retry.
POST /v1/kyc/webhook
{
  "webhook_url": "https://your-platform.com/kyc-notifications",
  "signing_secret": "whsec_<base64-encoded-key>"
}

Request fields

FieldTypeRequiredNotes
webhook_urlstringYesHTTPS URL where notifications are delivered
signing_secretstringNoYour Standard Webhooks signing secret in whsec_<base64-key> format, where the decoded key is at least 24 bytes (e.g. whsec_ followed by 32 random bytes in standard padded base64). Omit to receive unsigned webhooks.
You bring your own signing secret. Polymarket US never generates or returns a secret — you supply and keep it. Use the Standard Webhooks whsec_<base64-key> format (e.g. whsec_ followed by the output of openssl rand -base64 32). Registration is last-writer-wins: re-registering without a signing_secret clears any previously stored one (switching you to unsigned).

Response

{
  "validated": true,
  "message": "Webhook URL registered and validated successfully"
}
FieldTypeNotes
validatedbooltrue if the test POST to your URL succeeded
messagestringStatus / failure detail

Test-POST contract

Registration sends a POST to your URL with a normal notification envelope whose event_type is webhook.test and whose data is a fixed informational message. It is signed (same scheme as real deliveries) when you supplied a signing_secret. Your endpoint passes only by returning a 2xx. Redirects are not followed, and URLs that resolve to private or internal IP ranges are refused.

Receiving notifications

Polymarket US sends an HTTP POST with a JSON body to your registered URL.

Headers

Signed deliveries follow the Standard Webhooks convention. The three signing headers are present only when a signing secret is configured.
HeaderNotes
webhook-idStable, retry-invariant event ID — use it to deduplicate
webhook-timestampUnix seconds at send time
webhook-signatureSpace-delimited list of versioned signatures; currently a single v1,<base64(HMAC-SHA256)> entry
Content-Typeapplication/json

Body

Webhook data fields are snake_case (e.g. external_id, provisioned_participant), unlike the camelCase REST responses. See Field naming.
{
  "event_type": "kyc.approved",
  "event_id": "01J0...",
  "event_time": "2026-04-24T15:32:00Z",
  "data": {
    "external_id": "your-internal-user-id-123",
    "user_id": "your-internal-user-id-123",
    "kyc_eval_id": "<verification provider evaluation id>",
    "firm_id": "your-firm-id",
    "status": "KYC_STATUS_APPROVED",
    "provisioned_participant": "firms/ISV-Participant-YourFirmID/users/...",
    "status_set_at": "2026-04-24T15:32:00.123456789Z"
  }
}

Event types

event_typeMeaning
kyc.approvedParticipant approved and backend entities provisioned
kyc.rejectedParticipant rejected
webhook.testRegistration-time test event only (see above); never delivered afterward

data fields

Empty fields are omitted from the JSON.
FieldPresent onNotes
external_idbothThe identifier you supplied at start. Your correlation key; matches status.externalId and the GET /v1/kyc/status lookup
user_idbothSame value as external_id (legacy field name)
kyc_eval_idbothVerification-provider evaluation id, carried under its own field. Optional — present only when the provider id is exposed. Never use it as your correlation key
firm_idbothYour firm ID
statusbothKYC_STATUS_APPROVED or KYC_STATUS_REJECTED
status_set_atbothRFC 3339 timestamp of the decision
provisioned_participantkyc.approvedEngine-neutral participant identifier — opaque string. The only provisioning identifier you need
date_of_birthkyc.approvedThe participant’s date of birth may be included. Treat as sensitive PII
referral_code_ownedkyc.approvedPresent when an owned referral code is assigned
rejection_reasonkyc.rejectedFiner-grained rejection detail (provider sub-status)

Using these identifiers to trade

When you place an order on behalf of a participant, provisioned_participant is the “who” — pass it as the x-participant-id header on participant-scoped requests (trading, positions, reports). It is the only provisioning identifier you need.
KYC field (webhook)Same value in GET /v1/kyc/statusFormatWhat to do with it
provisioned_participantparticipantIdfirms/ISV-Participant-YourFirmID/users/...Send as the x-participant-id header — this is who the order is for
external_id / user_idstatus.externalIdyour own stringYour correlation key only — store it to map back to your user. Never sent to Polymarket US to identify the participant on a trading call
Use provisioned_participant, not external_id / user_id, to identify the participant on a trading call. The external_id (and its legacy duplicate user_id) is the identifier you supplied — it is meaningful only inside your own systems. The exchange identifies the participant by the provisioned_participant value carried in the x-participant-id header.
The webhook and GET /v1/kyc/status carry the same value under different field names — the webhook calls it provisioned_participant, while the status read calls it participantId.
# Placing an order for the participant from a kyc.approved webhook
curl -X POST https://api.polymarket.us/v1/orders \
  -H "Authorization: Bearer {access_token}" \
  -H "Content-Type: application/json" \
  -H "x-participant-id: firms/ISV-Participant-YourFirmID/users/your-internal-user-id-123" \
  -d '{
    "symbol": "tec-nfl-sbw-2026-02-08-kc",
    "side": "SIDE_BUY",
    "order_qty": 100,
    "price": 550,
    "type": "ORDER_TYPE_LIMIT",
    "time_in_force": "TIME_IN_FORCE_GOOD_TILL_CANCEL"
  }'

Delivery semantics

  • At-least-once. Deduplicate on event_id (the webhook-id header) — you may receive the same event more than once.
  • Retries. Only a 2xx counts as delivered. Redirects are not followed (a 3xx is a failed delivery); every non-2xx or transport error is retried with jittered exponential backoff up to a maximum attempt count. Retry-After is honored on 408 / 429. A repeatedly-failing endpoint is circuit-broken: deliveries pause for a cooldown that grows with each consecutive re-open, then resume.
  • Respond fast. Return 2xx promptly once you’ve durably accepted the event; do heavy processing asynchronously.

Verifying the signature

Because the secret is in the standard whsec_/base64 format, a Standard Webhooks SDK verifies deliveries out of the box — initialise it with the default constructor (e.g. NewWebhook in Go), passing the secret exactly as you registered it. No raw-key escape hatch (NewWebhookRaw) is needed. If you verify inline instead, the signed content is <webhook-id>.<webhook-timestamp>.<raw body> — the raw request bytes, before any JSON re-serialization. The HMAC key is the decoded Standard Webhooks key: strip the optional whsec_ prefix from your signing_secret and base64-decode the remainder using standard padded base64. Compute the expected signature, then constant-time compare it against each space-delimited candidate in webhook-signature; accept if any matches.
// Derive the key: drop the optional "whsec_" prefix, then base64-decode.
key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
if err != nil { /* reject: malformed secret */ }

// signed content: "<webhook-id>.<webhook-timestamp>." + raw body
mac := hmac.New(sha256.New, key)
mac.Write([]byte(id + "." + timestamp + "."))
mac.Write(body)
expected := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil))

for _, candidate := range strings.Fields(signatureHeader) {
    if hmac.Equal([]byte(candidate), []byte(expected)) {
        return true // valid
    }
}
return false // reject
Reject deliveries whose webhook-timestamp is too old to limit replay exposure, and always verify against the raw body you received rather than a re-encoded copy.

Next steps

Verification Flow

The synchronous outcomes that precede these webhooks.

Overview

How the KYC process fits together.