POST /v1/kyc/start with specific preprod input values.
Before this page you need:
- a public HTTPS endpoint that acknowledges promptly and does its processing afterwards,
- a webhook secret you generate yourself, format
whsec_<base64>, at least 24 bytes, - a working start call — see /kyc#start-a-verification,
- preprod credentials — preprod access does not require a signed agreement, and partners have lost weeks believing it did.
Webhooks
POST /v1/kyc/webhook registers the single URL where we deliver terminal KYC events, and the kyc.approved event is the only authoritative signal that a user can trade.
Registration
OnePOST /v1/kyc/webhook sets your firm’s webhook. Three properties of it will bite you:
- One URL per firm. There is no list, no per-event routing and no second URL.
- Last-writer-wins. A second registration silently replaces the first.
- Re-registering clears your signing secret. If you re-register to change the URL and do not set the secret again in the same call, you lose signature verification.
Rotating the secret without a gap
Because re-registration clears the secret, do it in this order:1
Accept both secrets in your receiver
Verify against the old secret and the new one, accepting either. Deploy that first.
2
Re-register with the new secret
Send the URL and the new secret in the same call. Anything you omit is gone, not preserved.
3
Confirm delivery, then drop the old secret
Wait for a real delivery that verifies against the new secret, then remove the old one.
GET to read back what is currently registered, so you cannot tell from our side whether the change took.
The event catalogue
Only terminal events fire. There is no review event, no DocV event, no provisioning event. Every non-terminal state must be polled with
GET /v1/kyc/status.
The UAT test plan currently contradicts this and implies a review event exists. It does not. If your test plan says to wait for one, that step cannot pass.
snake_case — including event_id — while REST responses on the same flow are camelCase. See /kyc#field-naming-changes-three-times-in-one-flow.
The participant ID on
kyc.approved is recorded as participantId, which cuts against the snake_case webhook convention. Accept both spellings until your integration lead confirms the shipped payload schema.Signature verification
We are Standard Webhooks conformant. The signed content is exactly:- Use the raw request bytes. Verify before you parse. A framework that decodes and re-serializes the JSON changes the bytes and every signature fails.
- Use the values from the headers as strings, unmodified. Do not reformat the timestamp.
- Compare in constant time.
whsec_<base64>, at least 24 bytes. Generate it with a CSPRNG, store it in your secret manager, and never put it in the repo that serves the endpoint.
Delivery semantics
Delivery is at-least-once. Dedupe onevent_id. Store every event_id you have processed and make the handler idempotent — a redelivered kyc.approved must not re-enable trading, re-credit anything, or re-send a user notification.
Acknowledge with a 2xx before you do any work, then do the work asynchronously. Verify the signature, dedupe on event_id, enqueue, return. Nothing that can be slow — a write to a cold shard, a downstream call, a user notification — belongs in front of the acknowledgement. A circuit breaker trips on repeated failures, so a slow or erroring receiver stops your deliveries, and with no GET to inspect your registration you will not see that from your side.
No numeric replay window is published. Our side describes a timestamp as “too old” without quantifying it, so enforce your own window in your receiver — the code above uses 300 seconds, which is your policy, not ours.
No outbound IP allowlist is published. Do not design an IP-based control and do not ask your security team to allowlist ours. The signature is the authentication.
Running more than one environment on one URL per firm
What is published is one URL per firm, last-writer-wins. Preprod and production are separate environments with separate credentials and separate keypairs, so you register in each one separately — but the limit itself is stated per firm, not per environment. The pattern that works:- One receiver per environment, at a distinct hostname, each with its own secret. Never point a production registration at a staging receiver; the terminal approval for a real user would land in a system that cannot act on it.
- Fan out behind your own URL. If staging, CI and a developer tunnel all need the same events, route them yourself from the single registered receiver. You cannot register a second URL, and trying to share one by re-registering means the last writer wins and the previous team silently stops receiving events.
- Record every registration on your side — URL, secret fingerprint, environment, timestamp, who ran it. There is no
GETto read this back, so your record is the only record you can check. - Never re-register from a deploy script. One
POST /v1/kyc/webhookin a CI job that runs on every merge will clear the signing secret of whatever was registered before it.
Sandbox fixtures
The fixtures below need a registered preprod webhook of their own — see Webhooks — because three of the four outcomes only become terminal there. Specific input values in preprod drivePOST /v1/kyc/start to a chosen outcome, so you can exercise all four branches without real people.
Verified fixtures
This is the shape of the table. Every cell that is not verified is marked as not published rather than filled with a plausible value.
Three things to read off it:
Paulina Gizelais the manual-review fixture, not the DocV fixture. Partners have used it to test document upload and got a case that sat in review instead. Manual review takes 1–2 business days if you did not senddocv_eligible: true.+12125551234is the DocV fixture. It is the input that returns adocvobject.- The
decisionandstatusliterals are not published for any outcome except the approved one, which isdecision: ACCEPTwithstatus: CLOSED. That is not a blocker — see /kyc#start-a-verification for branching that never reads those literals.
DocV links in preprod ask for a real ID today. There is no published synthetic document fixture, so finishing the preprod DocV flow means submitting an actual government ID through the hosted flow. Plan your DocV test around that, and do not ask a whole QA team to upload their passports. Confirm the current state with your integration lead before you schedule that testing.
Generate a unique SSN for every test user
This is the single most expensive preprod mistake on this surface. One SSN maps to exactly one exchange account, platform-wide. Reusing one test SSN across several test users points all of them at a single account. Transfers then fail withNOT_FOUND, and untangling it requires manual database cleanup on our side — it has already produced a 66-reply outage thread.
Generate a distinct SSN per fixture user, including throwaway ones, and treat HTTP 400 with {"code": 9, "message": "user already provisioned with a different SSN"} as a stop, not a retry. Note that this returns 400, not the documented 409. See /kyc#external-ids-participants-and-accounts.
What to do until the table is rebuilt
1
Drive only the three verified outcomes from fixtures
Manual review, DocV and reject are covered by the fixtures above. Use them.
2
Test the approved path through the webhook, not through a fixture
The approved branch you actually need to get right is “
ACCEPT arrives, participantId is empty, trading stays disabled until kyc.approved”. You can test that without a fixture: ignore participantId from the start response entirely and drive enablement from the webhook. The empty window runs from ~500 ms to 26 minutes, so this is the branch that breaks in production, not in tests.3
Assert on your own state machine, never on our literals
Write your tests against your own user states — unverified, provisioning, tradable, rejected. A test that asserts
decision == "ACCEPT" will break when an enum you never saw documented turns up, and "In Review" already proves that happens.4
Get any fixture you need confirmed by a run, in writing
If you need an outcome the table above does not cover, ask your integration lead to confirm it from an actual preprod run and record the date next to it in your own fixture file. Do not copy values from an older page — that is exactly how the published table drifted.
5
Keep a fallback for the fixtures that stop working
Assume any unverified fixture can change without a changelog entry. Make your test suite report “fixture produced an unexpected outcome” as a distinct failure from “our code is wrong”, so the next drift costs you an hour instead of a day.
Other preprod facts that affect KYC testing
- There is no preprod status page and no working partner-checkable preprod health endpoint. When preprod misbehaves you cannot distinguish our outage from your bug without asking us.
status.polymarketexchange.comcovers production only. - Preprod pools are funded manually by us on a Slack request. There is no self-service test funding, so plan the funding step of your test run with lead time — see /funding#the-funding-model.
{"code":12,"message":"unknown service connamara.ep3.v1beta1.KYCAPI"}in preprod means you called KYC over gRPC. Use REST.