Embed JSON API

The public aggregate behind every embed. Build your own card, bot, or dashboard from it.

Endpoint

GET https://api.poll.fun/api/embed/v1/polls/{identifier}

No token is needed. The identifier is a Poll slug or bet address. Eligibility is the same as the live card: public, not deleted, not flagged.

This endpoint is separate from the authenticated bet endpoints in the REST API. It returns aggregate numbers only and never includes participants.

Call it from your server. Browser requests from other origins are blocked by CORS today, so a client-side fetch on your own domain fails. Proxy through your backend or render on the server.

Example

$ curl https://api.poll.fun/api/embed/v1/polls/will-psg-be-the-ligue1-2027-champions

live response · refreshed every 15 seconds

json
{
  "data": {
    "schemaVersion": 1,
    "id": "4wwEfNLDLZUZis2yLxna27FFs1N7FLuYiUTKgTr3TP5",
    "slug": "will-psg-be-the-ligue1-2027-champions",
    "question": "Will PSG be the Ligue1 2027 champions?",
    "version": 3,
    "state": "OPEN",
    "outcomes": [
      {
        "outcomeIndex": 0,
        "label": "Yes",
        "totalOiMicros": "10000000",
        "shareBps": 8333,
        "wagerCount": 1,
        "isResolved": false
      },
      {
        "outcomeIndex": 1,
        "label": "No",
        "totalOiMicros": "2000000",
        "shareBps": 1667,
        "wagerCount": 1,
        "isResolved": false
      }
    ],
    "totalOiMicros": "12000000",
    "participantCount": 2,
    "closesAt": "2027-05-15T05:00:00.000Z",
    "resolutionAt": null,
    "resolutionCriteria": null,
    "coverImageUrl": null,
    "updatedAt": "2026-09-24T18:58:40.855Z",
    "canonicalUrl": "https://app.poll.fun/bet/will-psg-be-the-ligue1-2027-champions",
    "revision": "a2af67c55e49b478164cf2f1dcfeca61"
  },
  "error": null
}

Fields

FieldMeaning
schemaVersionContract version. It changes only on a breaking change.
idThe bet address on Solana. Stable for the life of the Poll.
slugHuman-readable identifier used in poll.fun URLs.
questionThe question as the creator wrote it.
versionPoll program version: 2 or 3 for two-outcome Polls, 4 for multi-outcome.
stateLifecycle state. See the states table.
outcomesOne entry per outcome, in creator order.
outcomes[].outcomeIndexZero-based position. Two-outcome Polls use 0 for the first option and 1 for the second.
outcomes[].labelOutcome text. Two-outcome Polls default to "For" and "Against" when the creator set no custom labels.
outcomes[].totalOiMicrosTotal backed on this outcome, in micro-USDC.
outcomes[].shareBpsThis outcome's share of the pot in basis points, 0 to 10000.
outcomes[].wagerCountNumber of funded picks on this outcome.
outcomes[].isResolvedTrue for the winning outcome once the Poll is resolved.
totalOiMicrosTotal pot in micro-USDC.
participantCountDistinct people with a funded pick.
closesAtISO 8601 time when picks close.
resolutionAtISO 8601 scheduled resolution time, when the creator set one.
resolutionCriteriaThe creator's rules text.
coverImageUrlCover art, when the creator set one.
updatedAtISO 8601 time of the last change to the Poll record.
canonicalUrlThe Poll page on poll.fun. Link here for picks.
revisionSHA-256 prefix of the payload, 32 hex characters. Equal revisions mean equal content.

Money and shares

All amounts are micro-USDC as decimal strings. One USDC is 1000000. Parse with BigInt, not Number, so large pots stay exact. Divide by one million only when you format for display.

typescript
const micros = BigInt(outcome.totalOiMicros);
const cents = (micros + 5_000n) / 10_000n;
const display = `$${cents / 100n}.${String(cents % 100n).padStart(2, "0")}`;

shareBps is the outcome's share of the pot in basis points. Divide by 100 for a percentage. Shares across all outcomes sum to 10000 when the pot is above zero, and to 0 when nothing has been backed yet.

States

StateMeaning
OPENPicks are open.
LOCKEDThe close time has passed. No new picks. Not yet resolving.
RESOLVINGFriends are voting on the outcome.
RESOLVEDThe outcome is decided. Payouts have not settled yet.
DISTRIBUTEDPayouts have settled on-chain.
CANCELEDThe creator canceled the Poll before it resolved.
REFUNDEDAll picks were refunded.

LOCKED is derived from closesAt at request time. A Poll can move from OPEN to LOCKED between two responses with no other field changing.

Caching and revision

Cache-Control: public, max-age=5, s-maxage=15, stale-while-revalidate=30

Poll every 10 to 15 seconds at most. Faster polling only returns cached copies. Compare revision between responses and skip your render when it has not changed. The official card does exactly this.

Errors

StatusBodyWhen
404{ "error": "Poll not found" }Unknown identifier, private Poll, deleted Poll, flagged Poll, or a malformed identifier. All look the same on purpose.
500{ "error": "Internal server error" }Temporary. Retry with backoff. Keep showing your last good response.

TypeScript

The endpoint is plain JSON, so fetch is enough. The types below match the contract.

embed-poll.ts
export type EmbedPollState =
  | "OPEN" | "LOCKED" | "RESOLVING" | "RESOLVED"
  | "DISTRIBUTED" | "CANCELED" | "REFUNDED";

export interface EmbedPollOutcome {
  outcomeIndex: number;
  label: string;
  totalOiMicros: string;
  shareBps: number;
  wagerCount: number;
  isResolved: boolean;
}

export interface EmbedPoll {
  schemaVersion: 1;
  id: string;
  slug: string | null;
  question: string;
  version: number;
  state: EmbedPollState;
  outcomes: EmbedPollOutcome[];
  totalOiMicros: string;
  participantCount: number;
  closesAt: string | null;
  resolutionAt: string | null;
  resolutionCriteria: string | null;
  coverImageUrl: string | null;
  updatedAt: string;
  canonicalUrl: string;
  revision: string;
}

export async function fetchEmbedPoll(identifier: string): Promise<EmbedPoll | null> {
  const response = await fetch(
    `https://api.poll.fun/api/embed/v1/polls/${encodeURIComponent(identifier)}`,
    { headers: { Accept: "application/json" } },
  );
  if (response.status === 404) return null;
  if (!response.ok) throw new Error(`Embed request failed: ${response.status}`);
  const body = (await response.json()) as { data: EmbedPoll; error: null };
  return body.data;
}