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-championslive response · refreshed every 15 seconds
{
"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
| Field | Meaning | |
|---|---|---|
| schemaVersion | 1 | Contract version. It changes only on a breaking change. |
| id | string | The bet address on Solana. Stable for the life of the Poll. |
| slug | string | null | Human-readable identifier used in poll.fun URLs. |
| question | string | The question as the creator wrote it. |
| version | number | Poll program version: 2 or 3 for two-outcome Polls, 4 for multi-outcome. |
| state | EmbedPollState | Lifecycle state. See the states table. |
| outcomes | EmbedPollOutcome[] | One entry per outcome, in creator order. |
| outcomes[].outcomeIndex | number | Zero-based position. Two-outcome Polls use 0 for the first option and 1 for the second. |
| outcomes[].label | string | Outcome text. Two-outcome Polls default to "For" and "Against" when the creator set no custom labels. |
| outcomes[].totalOiMicros | string | Total backed on this outcome, in micro-USDC. |
| outcomes[].shareBps | number | This outcome's share of the pot in basis points, 0 to 10000. |
| outcomes[].wagerCount | number | Number of funded picks on this outcome. |
| outcomes[].isResolved | boolean | True for the winning outcome once the Poll is resolved. |
| totalOiMicros | string | Total pot in micro-USDC. |
| participantCount | number | Distinct people with a funded pick. |
| closesAt | string | null | ISO 8601 time when picks close. |
| resolutionAt | string | null | ISO 8601 scheduled resolution time, when the creator set one. |
| resolutionCriteria | string | null | The creator's rules text. |
| coverImageUrl | string | null | Cover art, when the creator set one. |
| updatedAt | string | ISO 8601 time of the last change to the Poll record. |
| canonicalUrl | string | The Poll page on poll.fun. Link here for picks. |
| revision | string | SHA-256 prefix of the payload, 32 hex characters. Equal revisions mean equal content. |
States
| State | Meaning |
|---|---|
| OPEN | Picks are open. |
| LOCKED | The close time has passed. No new picks. Not yet resolving. |
| RESOLVING | Friends are voting on the outcome. |
| RESOLVED | The outcome is decided. Payouts have not settled yet. |
| DISTRIBUTED | Payouts have settled on-chain. |
| CANCELED | The creator canceled the Poll before it resolved. |
| REFUNDED | All 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=30Poll 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
| Status | Body | When |
|---|---|---|
| 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.
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;
}