Polymarket US (ISV) Agency Install

PU
← Developer Guide

Install the PU ISV widget on your web app

A step-by-step guide for embedding the Polymarket US (ISV) prediction widget. There are two placements: a public page where anyone can browse markets (clicking Place Order sends them to your login), and an in-app "Prediction" tab where a logged-in user completes KYC once and then trades.

Assumes the widget is already configured for your business in the admin builder (theme, category menu, webhook endpoints + secret, default trade limit). This guide only covers the two embeds + the account handoff. For the full reference (all attributes, the JS bridge, framework recipes, webhook payloads + verification) see the Developer Guide.

The user journey

Flow
Public page ──click Place Order──▶ redirect to /login ──▶ your app (user logs in)
                                                              │
                                                      "Prediction" tab
                                                              ▼
                        dashboard widget (PU) ── first time ──▶ KYC form ──▶ trade
                                              └─ returning ────▶ trade directly

Replace YOURBIZ with your business code throughout. The widget host in the snippets is filled in automatically.

Step 1 — Public page (browse + login redirect)

Use explore mode (explore.html) so there's no account. Set the no-account action to redirect and point it at your login page.

HTML
<div class="prediction-widget"
     data-src="https://widgets.example.com/explore.html"
     data-settings='{"market-platform":"PU","business":"YOURBIZ","accountless-cta":"redirect","explore_redirect_to":"/login","explore_cta_label":"Log in to trade","explore_cta_message":"Log in or sign up to place this trade."}'></div>
<script src="https://widgets.example.com/loader.js"></script>
  • accountless-cta: "redirect" overrides the PU default (the in-widget KYC form) — on a public page we want a redirect instead.
  • explore_redirect_to is a path on your site (or a full URL). When the user clicks Place Order, the widget asks your page to navigate there. explore_cta_label / explore_cta_message set the button + copy.

That's the whole public page — no account handoff, no webhooks.

Step 2 — In-app "Prediction" tab (dashboard + KYC)

Inside your authenticated app, render the widget in dashboard mode (widget.html). Pass your id for the logged-in user as pu-external-id (the KYC correlation key) and the user's spendable balance as pu-trade-limit.

HTML
<div class="prediction-widget"
     data-src="https://widgets.example.com/widget.html"
     data-settings='{"market-platform":"PU","business":"YOURBIZ","accountless-cta":"kyc","pu-external-id":"USER_123","pu-trade-limit":250}'></div>
<script src="https://widgets.example.com/loader.js"></script>

2a · Give the widget the account (returning users)

The Polymarket account id is the participantId produced by KYC. Store it against your user after onboarding (2c). On loads where you already have it, hand it to the widget so the user skips KYC:

JS
// Look up the participantId you stored for this user (from your backend).
const participantId = MY_APP.getStoredParticipantId(); // '' if not onboarded yet
if (participantId) PMWidgetFrame.setAccountId(participantId);

PMWidgetFrame.setAccountId() is the loader's API; it survives the widget's internal reloads, so call it once after mount (see Step 3 for timing).

2b · First-time users → KYC (automatic)

If you don't call setAccountId (no stored participantId), the widget knows the user isn't onboarded. When they go to place an order it shows the KYC form. If they return while verification is still pending it shows "verification in progress" instead of a second form — that gating uses the pu-external-id you passed. There is no login/redirect CTA here (that's what accountless-cta: "kyc" — the PU default — means).

2c · Capture the account when KYC completes

The moment onboarding succeeds, the widget posts a message to your page. Store the link (participantId ↔ your user) so future sessions skip KYC:

JS
window.addEventListener('message', (event) => {
  const d = event.data;
  if (!d || d.source !== 'prediction-spa' || d.type !== 'puAccountOnboarded') return;
  // d.detail = { externalId: 'USER_123', participantId: 'acct_…' }
  MY_APP.saveParticipantId(d.detail.externalId, d.detail.participantId);
});
Source of truth

The widget already adopts the new participantId for the current session, so the user can trade right away — persisting it is for their next visit. You also receive a signed kyc.approved webhook server-side; treat that as the authoritative record of the link (see Step 4).

Step 3 — Mounting inside a SPA tab

The loader auto-mounts every .prediction-widget on the page at load. For a tab that renders later (React / Vue / …):

  • Simplest: keep the widget's container in the DOM and show/hide the tab with CSS. It mounts once and preserves state.
  • Mount on demand: when the Prediction tab first opens, inject the container + the loader, then call setAccountId. See the Developer Guide's React and Next.js recipes for a drop-in mount/unmount.
Timing

setAccountId is a no-op until the widget's iframe exists. Call it after mount — e.g. on window load, or right after you inject the container — not at the top of the page script.

Step 4 — Webhooks (your server)

You configured the webhook URL(s) + secret for YOURBIZ in the admin builder. Your server receives signed events so you can link accounts and move balances:

EventDo
kyc.approvedPersist { externalId, participantId } to your user (the authoritative link).
order.placed/accepted/filled/rejectedRecord the trade and adjust the user's wallet balance.
position.updatedReconcile the net position.

Verify every delivery's HMAC signature before trusting it. Full payload shapes + a copy-paste verifier are in the Developer Guide's Webhooks and Verifying signatures sections.

Step 5 — The trade limit

pu-trade-limit is the user's current available balance/allowance (USD) — you own that number. The widget pre-checks it and the server enforces it (an over-limit order is rejected). Refresh it whenever the balance changes: re-render the embed (or reload the tab) with the new value.

Step 6 — Gate trades on balance / status (optional)

Beyond the per-order limit, you can decide at click time whether the user may trade at all — zero balance, an account hold, terms not accepted, whatever your rule is. Register a handler with PMWidgetFrame.onTradeValidation(); when the user clicks Place Order, the widget calls it (it can fetch) and waits for the verdict. Blocked → your message (plus an optional button) shows under the button and no order is sent; allowed → the order proceeds.

JS
// Register once, after the loader script has run.
PMWidgetFrame.onTradeValidation(async (ctx) => {
  const { balance } = await fetch('/api/me/wallet').then(r => r.json());
  if (balance > 0) return { allow_to_trade: true };
  return {
    allow_to_trade: false,
    message:      'Your balance is $0. Add credits to place a trade.',
    button_label: 'Add credit +',   // optional
    button_url:   '/wallet/add'     // optional (needs button_label)
  };
});
  • The handler is called on every click, so it always sees the current balance — no need to re-push on change. It may be async (return a Promise).
  • If it throws or doesn't answer within ~20 s, the widget fails closed (blocks with a "couldn't verify" notice) — a gate you registered must never be bypassed by an error.
  • The redirect button uses the same host-navigation channel as the login CTA — the user is sent to button_url on your site.
  • Prefer to push proactively instead? PMWidgetFrame.setTradeValidation(verdict) is the push alternative (pair it with the operator's Require parent trade validation option to fail closed). See the Developer Guide.

Full reference in the Developer Guide. This is a UX gate — keep your real authorization on the server.

Checklist

  • Public page: explore.html, accountless-cta:"redirect", explore_redirect_to → your login.
  • In-app tab: widget.html, market-platform:"PU", business, pu-external-id, pu-trade-limit.
  • Returning users: PMWidgetFrame.setAccountId(participantId).
  • Listen for puAccountOnboarded; persist participantId.
  • Optional: gate trades at click time with PMWidgetFrame.onTradeValidation(async (ctx) => verdict) (or push with setTradeValidation).
  • Server: receive + verify webhooks; link account on kyc.approved; move balances on order.*.