Polymarket API integration is harder than it looks because there are two separate products — Global and US — with different auth models, different identifiers, and different trading semantics. These are the 10 problems that cause the most production failures, with working fixes for each.

1. Using the Wrong Polymarket API

This is the single biggest source of wasted developer time. Polymarket is not one API — it is two independent platforms, each with multiple API surfaces.

Polymarket Global splits across three services: Gamma (gamma-api.polymarket.com) for market discovery and metadata, Data API (data-api.polymarket.com) for positions and trade analytics, and CLOB (clob.polymarket.com) for order books and trading. Polymarket US splits across gateway.polymarket.us for public browsing and api.polymarket.us for authenticated trading, portfolio, and WebSocket feeds.

Broken integrations almost always start with one of these mistakes: placing orders against a discovery endpoint, using Global token-ID logic against the US API, or trying US market slugs on Global CLOB endpoints.

# Global: discover on Gamma, trade on CLOB
import requests

markets = requests.get(
    "https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=5",
    timeout=20,
).json()

# US: discover on public gateway, trade on authenticated api
us_markets = requests.get(
    "https://gateway.polymarket.us/markets?limit=5",
    timeout=20,
).json()

Fix: treat these as two separate products in code. Create separate client modules — poly_global.py and poly_us.py — so identifiers, auth, and endpoints cannot cross-contaminate. The Polymarket US vs Global API guide covers the full architectural split. For a side-by-side comparison with Kalshi’s single-API model, see the prediction market API reference.

2. Global Auth: Misunderstanding L1 vs L2

Global CLOB authentication is two-layered, and most developers get confused about which layer does what.

L1 uses your wallet private key for EIP-712-based credential creation and for locally signing order payloads. L2 uses derived API credentials (apiKey, secret, passphrase) for authenticated CLOB HTTP requests. Developers often assume the L2 key is sufficient for everything, then wonder why order creation still fails — you still need L1 to sign the order itself.

from py_clob_client.client import ClobClient

client = ClobClient(
    "https://clob.polymarket.com",
    chain_id=137,
    key="0xYOUR_PRIVATE_KEY",
)
creds = client.create_or_derive_api_creds()
print(creds.api_key, creds.secret, creds.passphrase)

Fix: separate your code into four explicit stages: (1) wallet bootstrap / L1, (2) L2 credential derivation, (3) order signing with L1, (4) order posting with L2 headers. Verify each stage independently before composing them. The Polymarket auth troubleshooting guide walks through each failure mode in detail.

3. Global Wallet Type and Funder Mismatch

This causes one of the most common real integration failures on Global. The CLOB client needs the correct signatureType and funder address combination. The current types:

signatureTypeWallet TypeNotes
0EOAStandard Ethereum externally owned account
1POLY_PROXYPolymarket proxy wallet
2GNOSIS_SAFEMost common for new/returning users who don’t fit the other two

Wrong combination → "L2 AUTH NOT AVAILABLE - Invalid Signature." — with no further diagnostic detail from the API.

import { ClobClient } from "@polymarket/clob-client";
import { Wallet } from "ethers";

const signer = new Wallet(process.env.PRIVATE_KEY!);
const client = new ClobClient(
  "https://clob.polymarket.com",
  137,
  signer,
  apiCreds,
  0,                  // 0 = EOA
  signer.address      // funder
);

Fix: build a startup assertion that logs signer address, chosen signatureType, chosen funder, and whether the account is a proxy wallet. That one diagnostic block prevents hours of invisible auth debugging.

4. Global Approvals and Balances

Even when using relayer-assisted “gasless” flows, the funder must have the right token inventory and on-chain approvals. This catches developers who assume gasless means zero token setup.

The requirements: BUY orders need sufficient USDC.e allowance granted to the CTF exchange contract. SELL orders need sufficient conditional-token allowance. EOA users additionally need POL for gas, while proxy wallet users can use Polymarket’s gasless relayer.

import { ethers } from "ethers";
import { Interface } from "ethers/lib/utils";

const erc20Interface = new Interface([
  "function approve(address spender, uint256 amount) returns (bool)",
]);

const approveTx = {
  to: ADDRESSES.USDCe,
  data: erc20Interface.encodeFunctionData("approve", [
    ADDRESSES.CTF,
    ethers.constants.MaxUint256,
  ]),
  value: "0",
};

Fix: before every order burst, query balances and allowances once and cache for a few seconds. Most “not enough balance / allowance” errors are stale-local-state bugs, not actual insufficiency. The py-clob-client balance and allowance guide covers the exact query pattern.

5. Global Identifier Confusion

Polymarket Global uses five different identifiers for the same market, and developers constantly pass the wrong one to the wrong endpoint.

IdentifierWhere to Use It
SlugHuman-readable URL paths, Gamma lookup
Condition IDAnalytics endpoints, Data API
Token ID / Asset IDCLOB /book, /price, order creation
Market AddressOn-chain contract interaction
Question IDResolution and oracle queries

A market maps to a pair of CLOB token IDs (YES and NO outcomes). Gamma is where you discover markets and extract all identifier types. CLOB trading endpoints work exclusively on token IDs.

import requests

m = requests.get(
    "https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=1",
    timeout=20,
).json()[0]

# Gamma market object contains all identifier types
print(m["question"])
print(m.get("outcomes"), m.get("outcomePrices"))
# Extract token_id for CLOB trading

Fix: define one internal dataclass and force every downstream function to accept it instead of raw strings:

from dataclasses import dataclass

@dataclass
class GlobalMarketRef:
    slug: str | None
    condition_id: str | None
    token_ids: list[str]   # [YES_token, NO_token]

This eliminates the entire class of “wrong identifier for wrong endpoint” bugs. The Gamma API guide covers the full market object schema and how to extract each identifier type.

6. Global Order Validation Failures

Global order creation has a surprisingly large surface for soft failures. Every order requires two fields that developers routinely omit: tickSize and negRisk.

Current validation constraints:

  • Prices must conform to the market’s tick size (typically "0.01" or "0.001")
  • Multi-outcome markets require negRisk: true
  • post-only is only valid with GTC or GTD time-in-force
  • Batch placement is capped at 15 orders per request
  • Common rejection reasons: invalid tick size, insufficient balance, duplicate order, post-only crossing, FOK not filled
import { Side, OrderType } from "@polymarket/clob-client";

const response = await client.createAndPostOrder(
  {
    tokenID: "TOKEN_ID",
    price: 0.50,
    size: 10,
    side: Side.BUY,
  },
  {
    tickSize: "0.01",
    negRisk: false,
  },
  OrderType.GTC
);

Fix: preflight every order with a local validator before submission:

from decimal import Decimal

def validate_order(price, tick_size, neg_risk_market, neg_risk_order,
                   order_type, tif, batch_len):
    errors = []
    if not (Decimal("0") < Decimal(str(price)) < Decimal("1")):
        errors.append("price out of range")
    if Decimal(str(price)) % Decimal(tick_size) != 0:
        errors.append(f"price not aligned to tick {tick_size}")
    if neg_risk_market != neg_risk_order:
        errors.append("negRisk mismatch")
    if order_type == "POST_ONLY" and tif not in ("GTC", "GTD"):
        errors.append("post-only requires GTC or GTD")
    if batch_len > 15:
        errors.append(f"batch size {batch_len} exceeds max 15")
    return errors

7. Heartbeat Timeouts and Sports-Market Cancels

Two Global-specific gotchas that produce “mysterious” order cancellations.

Heartbeat requirement (added January 2026). If a valid heartbeat is not received within 10 seconds plus a 5-second buffer, Polymarket cancels all your open orders. This is not optional for any bot that rests orders.

let heartbeatId = "";
setInterval(async () => {
  const resp = await client.postHeartbeat(heartbeatId);
  heartbeatId = resp.heartbeat_id;
}, 5000);  // send every 5s to stay within the 15s window

Sports-market auto-cancellation. Outstanding limit orders are automatically canceled when the game begins. Marketable orders can also have a 3-second placement delay before matching. If you are market-making Global sports markets, your bot needs to know the event start time and proactively flatten positions before kickoff.

Fix: store the scheduled start time with every sports-market order. Set a timer to cancel or flatten 60 seconds before game start, rather than waiting for the exchange-side sweep. For agents that trade sports across both Polymarket and offshore sportsbooks, the agent betting stack architecture places this scheduling logic in Layer 4 (Intelligence).

8. US Auth: It Is Not the Same Model as Global

Polymarket US has a completely different authentication system. Reusing Global signing middleware will not work.

US auth flow: create an app account, complete identity verification, generate keys in the developer portal. The docs warn to keep using the same sign-in method (Apple, Google, or email) or API-key access may break.

Requests use three headers: X-PM-Access-Key, X-PM-Timestamp, and X-PM-Signature. The signature signs timestamp + method + path using Ed25519, and timestamps must be within 30 seconds of server time.

import base64, time, requests
from cryptography.hazmat.primitives.asymmetric import ed25519

private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
    base64.b64decode("YOUR_SECRET_KEY")[:32]
)

def us_auth_headers(method: str, path: str) -> dict:
    ts = str(int(time.time() * 1000))
    msg = f"{ts}{method}{path}"
    sig = base64.b64encode(private_key.sign(msg.encode())).decode()
    return {
        "X-PM-Access-Key": "YOUR_KEY_ID",
        "X-PM-Timestamp": ts,
        "X-PM-Signature": sig,
        "Content-Type": "application/json",
    }

r = requests.get(
    "https://api.polymarket.us/v1/portfolio/positions",
    headers=us_auth_headers("GET", "/v1/portfolio/positions"),
    timeout=20,
)

Fix: build a dedicated US signer module. Do not parameterize your Global signer to “also handle” US — the cryptographic primitives are different (Ed25519 vs EIP-712/secp256k1), the header names are different, and the signed message format is different. The Polymarket US API guide covers the full endpoint reference and auth implementation.

9. US NO-Side Pricing Semantics

This is the most dangerous US trading bug because it produces valid-looking orders that do the wrong thing.

Polymarket US only allows trading the YES/long side directly. The NO/short side is synthetic exposure through order intent. The critical rule: price.value always refers to the YES/long side price, regardless of your intent.

If you want to buy NO at $0.83, you send a YES price of $0.17 with ORDER_INTENT_BUY_SHORT. If you accidentally send 0.83 as the price with ORDER_INTENT_BUY_SHORT, you are buying NO at an implied $0.17 — the exact opposite of what you intended.

{
  "marketSlug": "aec-cbb-usc-iowa-2026-01-28",
  "type": "ORDER_TYPE_LIMIT",
  "price": { "value": "0.17", "currency": "USD" },
  "quantity": 10,
  "tif": "TIME_IN_FORCE_GOOD_TILL_CANCEL",
  "intent": "ORDER_INTENT_BUY_SHORT"
}

Fix: write one helper and never build price logic inline again:

from decimal import Decimal

def us_api_price(desired_price: str, intent: str) -> str:
    """Convert desired outcome price to Polymarket US API price (always YES)."""
    if intent in ("ORDER_INTENT_BUY_LONG", "ORDER_INTENT_SELL_LONG"):
        return desired_price
    if intent in ("ORDER_INTENT_BUY_SHORT", "ORDER_INTENT_SELL_SHORT"):
        return str(Decimal("1.00") - Decimal(desired_price))
    raise ValueError(f"Unknown intent: {intent}")

# Want to buy NO at 0.83 → send YES price 0.17
api_price = us_api_price("0.83", "ORDER_INTENT_BUY_SHORT")
# api_price == "0.17"

This same YES-price-only convention differs from Kalshi’s explicit side + action model. If your agents trade across both platforms, the cross-market arbitrage guide covers how to normalize pricing across Polymarket US, Polymarket Global, and Kalshi into a single internal representation.

10. US Rate Limits and Over-Polling

Polymarket US enforces aggressive rate limits and the documentation is explicit: stop polling and use WebSocket streams instead.

ScopeLimit
Global2,000 requests / 10 seconds
Order placement440 / 10 seconds
Order queries55 / 10 seconds

Additional traps: synchronousExecution: true can wait up to 10 seconds for final state. This is useful for market orders where you want confirmation, but wasteful for ordinary resting limit orders. Use async submission and then stream order status via WebSocket.

import time

def with_backoff(fn, max_retries=4):
    for attempt in range(max_retries):
        resp = fn()
        if resp.status_code != 429:
            return resp
        wait = 2 ** attempt
        time.sleep(wait)
    raise RuntimeError("Max retries exceeded")

The clean US production pattern:

  1. Public gateway API for market discovery
  2. Preview order via REST
  3. Async order submission via REST
  4. Private WebSocket for order, position, and balance updates
  5. Market WebSocket for book and trade streaming

The Polymarket rate limits guide covers the full per-endpoint breakdown. For WebSocket implementation patterns including reconnect logic and message parsing, see the Polymarket WebSocket guide.

Global vs US Quick Reference

DimensionPolymarket GlobalPolymarket US
Discovery APIGamma (gamma-api.polymarket.com)Gateway (gateway.polymarket.us)
Trading APICLOB (clob.polymarket.com)Authenticated (api.polymarket.us)
Auth modelWallet + L1/L2 (EIP-712 + derived creds)API keys + Ed25519 signing
Trade identifierToken ID / Asset IDMarket slug
Price semanticsDirect YES/NO token pricingYES price only, intent determines side
HeartbeatRequired (15s window)Not required
Order batch limit15 per requestPer rate limit tier
ChainPolygon (chain ID 137)Custodial (no on-chain interaction)

The Polymarket Integration Golden Path

For Global integrations:

  1. Use Gamma for discovery, CLOB for trading. Never mix these surfaces.
  2. Get L1 + L2 auth right first. Verify wallet bootstrap, credential derivation, order signing, and order posting as four independent steps.
  3. Log signatureType and funder at startup. The “Invalid Signature” error tells you nothing; your diagnostic block tells you everything.
  4. Run the heartbeat loop. If you rest orders, heartbeats are not optional.
  5. Preflight every order for tick size, negRisk, and batch limits.

For US integrations:

  1. Build a dedicated Ed25519 signer. Do not adapt your Global auth code.
  2. Always convert NO prices to YES prices before submission. Use a helper function, never inline math.
  3. Use WebSockets for all real-time data. The rate limits are designed to push you toward streams.
  4. Preview before submitting. The preview endpoint catches validation errors without consuming rate limit budget on the order endpoint.
  5. Use async submission for resting limit orders. Reserve synchronousExecution for market orders only.

For the full Polymarket Global API reference, start with the Polymarket API guide. For the US platform, see the Polymarket US API guide. To compare both against Kalshi’s single-API architecture, the prediction market API reference has the cross-platform comparison. For the Kalshi equivalent of this guide, see the top 10 Kalshi API problems.