Public API Reference
The Tokei Public API lets you manage promotions, create entries programmatically, pull analytics and leaderboards, and receive webhooks when participants enter. Every endpoint returns JSON, and all of them are versioned under /api/v1 and need an API key — except GET /api/public/contests, the unauthenticated listing of what is live on Tokei right now. A machine-readable OpenAPI 3.0 spec is also available.
Prefer not to write the HTTP calls yourself? tokei-agent wraps every endpoint below as a CLI and an MCP server, so Claude Code, Claude Desktop, OpenClaw and other AI agents can run your campaigns directly.
Quick Start
Getting Started
- Log in to your Tokei dashboard and go to Dashboard > Settings > API.
- Click Create New Key, name it, and copy the full key — it is shown only once.
- Verify the key works:
curl -X GET "https://tokei.io/api/v1/me" \
-H "Authorization: Bearer YOUR_API_KEY"Then list your promotions and create your first entry:
curl -X GET "https://tokei.io/api/v1/contests" \
-H "Authorization: Bearer YOUR_API_KEY"
curl -X POST "https://tokei.io/api/v1/contests/YOUR_CONTEST_ID/entries" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "name": "Test User", "points": 5}'Naming
Terminology
The API and the dashboard use different words for the same objects. They are not different things — this table is the whole mapping.
| API says | Dashboard / people say | Notes |
|---|---|---|
| contest | page, campaign, promotion | The core object. contestId in a path is the page's id. |
| promotion | page, campaign | The same object again — POST /promotions creates it, and it reads back under /contests/:contestId. |
| entry | signup, subscriber, entrant | One person joining a page. |
| title | headline | The visible page headline — not the internal dashboard name (campaign_name, read-only). |
| settings.template | skin, design | basic-new, showcase or future. |
The CLI names commands after the dashboard words (pages:list) while the JSON it returns uses the API words.
Auth
Authentication
The base URL is https://tokei.io/api/v1. Every request must include your API key in the Authorization header:
Authorization: Bearer tokei_k_x8Kp2mNq4rT6vW9yB1dF3gH5jL7nP0sU- Keys start with
tokei_k_followed by 32 random characters. - Up to 5 active keys per account; keys access all promotions you own.
- The full key is shown only once at creation — store it securely.
- Revoke keys anytime from the dashboard; revocation is immediate.
- API access requires an active subscription or lifetime plan (trial accounts receive 403).
Versioning
Versioning & Deprecation
Every authenticated endpoint lives under /api/v1. Breaking changes ship under a new version prefix — /api/v2 — rather than by mutating /api/v1, so an integration written against v1 keeps working.
If a version is ever retired, it will be announced ahead of time: the affected endpoints will carry a Sunset response header naming the retirement date, alongside a published migration timeline. No version has been deprecated to date. The one endpoint outside this scheme is GET /api/public/contests, which is unauthenticated and unversioned by design.
Errors
Error Handling
| Status | Code | Description |
|---|---|---|
| 200 | OK | Request succeeded. |
| 201 | CREATED | Resource created. |
| 400 | BAD_REQUEST | Invalid parameters. |
| 401 | UNAUTHORIZED | Missing, invalid, or revoked API key. |
| 403 | FORBIDDEN | Valid key but insufficient plan or missing scope. |
| 404 | NOT_FOUND | Promotion or resource not found. |
| 409 | CONFLICT | Duplicate entry (email already entered this promotion) or reused idempotency_key. |
| 409 | WINNERS_ALREADY_SELECTED | An end_date write — setting or clearing — on a promotion whose winners have already been drawn. The deadline is frozen once a draw has happened. |
| 413 | PAYLOAD_TOO_LARGE | Request body exceeds the 10KB limit (PATCH /api/v1/contests/:contestId alone is raised to 64KB, to fit a full entry_methods array). |
| 422 | VALIDATION_ERROR | Request body failed validation — see the details array. |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests — see the Retry-After header. |
| 500 | INTERNAL_ERROR | Server error. |
Errors always use a structured body:
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "You have exceeded the rate limit of 60 requests per minute.",
"status": 429
}
}Validation failures (422) include a details array:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body failed validation.",
"status": 422,
"details": [
{ "field": "email", "message": "Email is required." },
{ "field": "points", "message": "Points must be a positive integer." }
]
}
}Limits
Rate Limiting
| Plan | Read / min | Write / min | Daily cap |
|---|---|---|---|
| Subscriber | 60 | 30 | 10,000 |
| Lifetime | 120 | 60 | 50,000 |
Every response includes rate-limit headers; 429 responses also include Retry-After:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1708200000The daily cap counts all requests (reads and writes) per account within a UTC calendar day. Exceeding it returns 429 RATE_LIMIT_EXCEEDED with Retry-After set to the seconds until UTC midnight.
- Cache responses when possible and fetch in pages.
- Implement exponential backoff on 429.
- Use webhooks instead of polling for real-time updates.
Lists
Pagination
All list endpoints accept page (default 1) and per_page (default 25, max 100) and return a pagination object:
{
"success": true,
"data": [ ... ],
"pagination": { "page": 1, "per_page": 25, "total_pages": 4, "total_count": 87 }
}No API key required
GET /api/public/contests
A public, unauthenticated list of every promotion that is live on Tokei right now — built for directories, aggregators and agents that want to browse rather than manage. Send no Authorization header: this route sits under /api/public, outside the /api/v1 base every other endpoint on this page uses, and ignores API keys entirely.
“Live” means the same thing it means in sitemap.xml: an active promotion with either no end date or an end date in the future. Every live promotion is listed — there is no per-creator suppression, so this feed and sitemap.xml always agree. Responses are cached, so a listing can be up to about 30 minutes old.
| Query parameter | Default | Description |
|---|---|---|
| page_type | all four | Comma-separated subset of contest, levelup, promote, discover. An unrecognised value is a 400, not an empty list. |
| limit | 50 | Rows per response, 1–100. |
| offset | 0 | Rows to skip. total is the unpaginated count, so page until offset >= total. |
url is the canonical public address — the same one sitemap.xml lists, so it never redirects. description is plain text (the stored value is creator HTML and is stripped here). image is the promotion's own picture where it has one, falling back to the creator's account-level image; a video is used only when it can be turned into a still. image and ends_at can each be null, and prizes is empty for promotions that are not giveaways.
entries is the total on the promotion page's counter. Entrants earn several entries each, so it is larger than the number of people. It is null when the creator hides the counter. region.mode is worldwide, allowlist (only the listed countries) or blocklist (everyone except them), and region.countries holds ISO country codes with any continent already expanded. host is the sponsor named in the Official Rules, or null. Promotions have no separate start date, so treat created_at as the start.
Each prize is the same object the authenticated GET /v1/contests/{id} returns — name and winners are always present, value is a number and is omitted along with currency when the creator did not set them.
curl "https://tokei.io/api/public/contests?page_type=contest,levelup&limit=20"
{
"success": true,
"data": {
"contests": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Win a 1961 Denizen Chronograph Watch",
"description": "Enter for a chance to win a restored 1961 chronograph.",
"url": "https://tokei.io/contest/9CV9Pp",
"page_type": "contest",
"image": "https://cdn.tokei.io/og-images/watch.jpg",
"prizes": [
{
"name": "1961 Denizen Chronograph",
"winners": 1,
"value": 480,
"currency": "usd"
}
],
"entries": 788,
"region": { "mode": "allowlist", "countries": ["GB", "US"] },
"referral_bonus": true,
"host": "Denizen Watches",
"created_at": "2026-08-01T09:12:00Z",
"updated_at": "2026-08-20T14:03:00Z",
"ends_at": "2026-09-30T23:59:00Z"
}
],
"total": 4,
"limit": 20,
"offset": 0,
"generated_at": "2026-08-27T11:00:00Z"
}
}Agents
CLI — tokei-agent
tokei-agent is the official command-line client for this API — a zero-dependency npm package that prints JSON to stdout, so you can pipe it into jq or hand it straight to an AI agent. Everything it does is available over plain HTTP too; the CLI just saves you writing the request. It also doubles as an MCP server.
npm install -g tokei-agent
# or run it without installing:
npx tokei-agent --helpRequires Node 22.12+. Published on npm; the agent landing page is at tokei.io/agent.
Configuration
| Variable | Required | Meaning |
|---|---|---|
| TOKEI_API_KEY | Yes | Sent as Authorization: Bearer <key>. Create one at Dashboard > Settings > API. |
| TOKEI_API_URL | No | Base URL override (default https://tokei.io). |
export TOKEI_API_KEY=tokei_k_... # bash / zsh
set -x TOKEI_API_KEY tokei_k_... # fish
$env:TOKEI_API_KEY = "tokei_k_..." # PowerShell
tokei-agent me # verify the key, see plan + API usage
tokei-agent pages:list --status active
tokei-agent stats <contestId>Output envelope and exit codes
stdout carries the API's JSON body exactly as documented on this page, plus a top-level rate_limit object built from the X-RateLimit-* headers (null when those headers were absent, e.g. a network failure). Read it and self-throttle rather than discovering a 429.
{ "success": true, "data": { ... },
"rate_limit": { "limit": 60, "remaining": 59, "reset": 1753000000 } }| Exit code | Meaning |
|---|---|
| 0 | Success (HTTP 2xx). |
| 1 | API or network error. The JSON error body is still printed on stdout; pure network failures print {"ok": false, "error": {"type": "network_error", ...}}. |
| 2 | Usage error (bad flags, missing argument, missing TOKEI_API_KEY) — printed as JSON on stderr. Nothing was sent to the API. |
Known issue — exit codes on Node 24 / Windows (fixed in 0.3.0). On 0.2.2 and earlier the CLI could print correct JSON and then abort during process exit, corrupting the exit code ($LASTEXITCODE read -1073740791 on success and failure alike). On an affected version, judge a run by the JSON on stdout — or upgrade. (Historical labelling slip: the 0.3.0 tarball misreported --version as 0.2.2; 0.3.1 and later report correctly.)
Agents
Command Reference
Every command maps to one endpoint on this page. Command names use the words humans use ("pages"); the JSON they return uses the API's words ("contest"/"promotion") — see Terminology. Read commands work with any key; write commands need a read+write key.
Read (any key)
| Command | Endpoint | Does |
|---|---|---|
| me | GET /me | Verify the key; account, plan, API usage |
| pages:list | GET /contests | List pages — --status, --mode, --page, --per-page |
| pages:get <contestId> | GET /contests/:contestId | One page in full (prizes, reward tiers, media, public URL) |
| stats <contestId> | GET /contests/:contestId/analytics | Aggregated analytics |
| leaderboard <contestId> | GET /contests/:contestId/leaderboard | Participants ranked by points |
| referrals:top <contestId> | GET /contests/:contestId/referrals | Top referrers, ranked by conversions |
| winners:list <contestId> | GET /contests/:contestId/winners | Selection-run history, newest first, with each run's winners |
| entries:list <contestId> | GET /contests/:contestId/entries | Signups — filter with --email |
| surveys:list <contestId> | GET /contests/:contestId/survey-responses | Survey responses |
| webhooks:list | GET /webhooks | List webhook subscriptions (no write scope needed) |
| templates:list | GET /templates | Named starting points, for pages:clone --template |
| actions:catalog | GET /actions/catalog | Every entry-action type Tokei supports — filter with --type |
| events:catalog | GET /events/catalog | Every webhook event Tokei's delivery engine understands — filter with --type |
Write (read+write key)
| Command | Endpoint | Does |
|---|---|---|
| pages:clone | POST /promotions | Create a page from one you own, a named template, or the starter. 20/day cap |
| media:upload <file> | POST /media, then PUT to storage | Upload an image or video and get back a public_url. ≤4MB per file (video too) |
| pages:update <contestId> | PATCH /contests/:contestId | Update title, description, dates, prizes, reward tiers, appearance and media |
| pages:publish <contestId> | PATCH /contests/:contestId | Sugar for {"status": "active"} — needs a future end_date |
| pages:unpublish <contestId> | PATCH /contests/:contestId | Sugar for {"status": "draft"} — blocks new signups, but the page still renders publicly |
| entries:create <contestId> | POST /contests/:contestId/entries | Add a signup |
| webhooks:create | POST /webhooks | Subscribe an HTTPS endpoint to one or more of the 5 events — the whsec_ secret is shown once |
| webhooks:delete <webhookId> | DELETE /webhooks/:webhookId | Remove a subscription |
Write commands take simple fields as flags, and full or nested bodies via --data '<json>' or --data @file.json (flags win on conflict). Fields only reachable through --data: prizes, reward_thresholds, metadata, and explicit nulls. The CLI does no local schema validation — the API's 422 with per-field error.details is the validation.
# a full build, end to end
tokei-agent me # verify key + plan first
tokei-agent templates:list # never hardcode a slug
PAGE=$(tokei-agent pages:clone --title "Spring Launch Waitlist" \
--template product-hunt | jq -r '.data.id')
HERO=$(tokei-agent media:upload ./hero.png | jq -r '.data.public_url')
tokei-agent pages:update "$PAGE" \
--description "Join the list for early access." \
--template showcase --dark-mode true --primary-color "#7d78c6" \
--image-video "$HERO"
tokei-agent pages:publish "$PAGE" --data '{"end_date":"2026-09-01T00:00:00Z"}'
tokei-agent stats "$PAGE"Run tokei-agent --help for every flag.
Agents
MCP Server
tokei-agent mcp runs a local Model Context Protocol server over stdio, exposing all 21 commands as MCP tools for Claude Code, Claude Desktop, OpenClaw, and any other MCP client. No extra install — it is the same package.
claude mcp add tokei --env TOKEI_API_KEY=tokei_k_... -- npx -y tokei-agent mcpOr in a client's JSON config:
{
"mcpServers": {
"tokei": {
"command": "npx",
"args": ["-y", "tokei-agent", "mcp"],
"env": { "TOKEI_API_KEY": "tokei_k_..." }
}
}
}- Tool names swap
:for_—pages:listbecomespages_list. - Inputs use this API's wire field names directly (
contest_id,per_page,prizes, …), so nested bodies need no--data. - Results carry the same envelope (including
rate_limit) as text content, withisErrorset on API failures — the error semantics above apply unchanged.
Agents
Using Tokei from an AI Agent
The package ships an agent-oriented reference, SKILL.md, which agents like Claude Code and OpenClaw discover automatically — worked examples, error-handling guidance, and the gotchas below. These four are the ones that bite hardest, so they are worth knowing whether you drive the API by CLI, by MCP, or by raw HTTP.
- Check the key and plan first. API access requires an active subscription or lifetime plan — trial accounts get
403on every call, so a whole workflow can fail on its first request for a reason no other error explains. Note thatmereports the plan but not the key's scope: you discover a read-only key by getting403 FORBIDDENon your first write. - Media must be uploaded through Tokei first. The seven media fields on
PATCH /api/v1/contests/:contestIdare guarded by a host allowlist, so local paths and third-party URLs are rejected. Upload, then patch thepublic_urlyou get back — see Upload Media. - List fields replace wholesale.
prizes(max 50) andreward_thresholds(max 50) are not merged — whatever array you send becomes the entire list. Always read, modify, then write the complete list back. - Unpublishing hides the page.
{"status": "draft"}stops new signups, and visitors to the URL see “This promotion isn’t open right now” (noindex). Only the owner, signed in, still sees the page. Entries are kept, and publishing again restores it.
Agent-safe keys, human in the loop
Tokei is built so agents draft and humans approve:
- Give monitoring and reporting agents a read-only key; reserve read+write keys for agents that genuinely need to change things.
- Set an expiry on keys you hand to an agent; an expired key returns 401.
- Revoke from Dashboard > Settings > API at any time — revocation is immediate.
- New webhook subscriptions created through the API trigger a security notification to the account owner.
- Have the agent create pages as drafts and leave publishing to you — a draft is fully readable and editable, and only
statusseparates it from live. - API responses can include entrant emails and survey answers. They land in the agent's conversation history — treat that output as the personal data it is.
More: the agent landing page, the npm package (README and SKILL.md are bundled), and the OpenAPI spec.
Account
GET /api/v1/me
Verifies your API key and returns account information including plan, today's API usage, and the number of active promotions (the active_contests field name is retained for backwards compatibility).
{
"success": true,
"data": {
"user_id": "usr_a1b2c3d4",
"email": "you@example.com",
"plan": "lifetime",
"api_usage": {
"requests_today": 142,
"daily_limit": 50000,
"rate_limit_per_minute": 120
},
"active_contests": 12
}
}Promotions
List & Retrieve Promotions
These read endpoints live under /api/v1/contests/… for backwards compatibility — they return your promotions, and the id returned by POST /api/v1/promotions works with all of them.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/contests | List all your promotions |
| GET | /api/v1/contests/:contestId | Retrieve a specific promotion |
The list endpoint supports status (draft, active, completed, deleted — exactly the values a promotion's status can hold) and mode (competition, gamification, sharing_only) filters plus pagination. A promotion object looks like:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Win a Nintendo Switch",
"description": "Enter free for a chance to win a Nintendo Switch.",
"status": "active",
"mode": "competition",
"type": "promotion",
"contest_url": "win-nintendo-switch",
"public_url": "https://tokei.io/contest/win-nintendo-switch",
"start_date": "2026-02-01T00:00:00Z",
"end_date": "2026-03-01T23:59:59Z",
"days_left": 12,
"total_entries": 847,
"total_points_awarded": 4235,
"entry_methods": [
{ "label": "Enter with Email", "points": 5, "actionType": "email_signup" },
{ "label": "Follow us on Twitter", "points": 3, "actionType": "twitter_follow" }
],
"settings": { "template": "basic-new", "color": "#6366F1" },
"template": "basic-new",
"primary_color": "#6366F1",
"card_width": "max-w-2xl",
"image_video": "https://media.tokei.io/contest-images/hero.jpg",
"secondary_image": null,
"third_image": null,
"fourth_image": null,
"fifth_image": null,
"background_image": null,
"og_image": null,
"campaign_name": "Switch giveaway (internal)",
"project_name": "Free entry, ships worldwide",
"prizes": [{ "name": "Nintendo Switch", "winners": 1, "value": 299, "currency": "USD" }],
"reward_thresholds": [],
"daily_bonus_enabled": true,
"referral_enabled": true,
"dark_mode_enabled": false,
"created_at": "2026-01-25T10:30:00Z",
"updated_at": "2026-02-15T14:22:00Z"
}title is the visible page headline, not the internal dashboard name — project_name is the small subheading and campaign_name is the internal name; both are read-only. Appearance and media (primary_color, card_width, and the image fields) read back what the page actually renders. settings.color is a deprecated alias of primary_color.
status is the effective status — draft, active, completed or deleted — derived from the stored value and the dates: a promotion whose end_date has passed reads as completed, and one that has not started yet reads as draft. The ?status= filter matches that same derived value, so anything you read back is something you can filter on. Likewise days_left counts down from end_date on every read and is 0 once the promotion has ended, and each entry_methods[].points is what that action actually awards, including any per-action value the owner has configured.
total_entries counts entry actions (one row per completed entry method, not per person) — the same count as total_entries in the analytics response. total_points_awarded is the points total across those actions. If you want the number of distinct people — usually what “entries” means to a creator — use unique_participants from the analytics endpoint instead.
Promotions
Update a Promotion
| Method | Endpoint | Description |
|---|---|---|
| PATCH | /api/v1/contests/:contestId | Update copy, dates, prizes and reward thresholds (write scope) |
Send at least one field. Unknown fields are rejected. The body limit is 64KB on this endpoint only (every other write endpoint stays at 10KB), to fit a full 30-row entry_methods array.
| Field | Accepts |
|---|---|
| title | 1–100 chars. Sets the dashboard name and the visible page headline. |
| description | ≤2000 chars or null. Basic rich-text HTML; other tags are stripped. |
| start_date | ISO 8601 or null. A future date pauses new entries until then. |
| end_date | Future ISO 8601 or null. Recomputes days_left. Setting or clearing it after winners are drawn returns 409 WINNERS_ALREADY_SELECTED. Clearing it on an active promotion returns 422, so unpublish in the same request or send a new date. |
| prizes | Up to 20 of { name, winners, value?, currency? }. Replaces the list. |
| reward_thresholds | Up to 50 of { id, points, rewardType, rewardDescription, rewardDetails?, isEnabled }. Replaces the list and recomputes the VIP threshold from the cheapest enabled vip_status tier. |
| template | basic-new (default, classic entry list), showcase (two-column), future (dark, game-style) or simple (Custom: bare markup styled by custom_css). |
| custom_css | ≤20,000 chars or null. Styles the Custom template on the hosted page and widget via --tokei-* properties and .tokei-simple-* classes. An unsafe construct returns 422 VALIDATION_ERROR naming the reason. |
| dark_mode_enabled | Boolean. A creator toggle; visitors' color scheme is ignored. |
| primary_color | Hex only (3, 4, 6 or 8 digits, e.g. #7d78c6), or null for the template default. Other CSS color formats are rejected. |
| card_width | narrow, medium, wide, xl, or the stored class max-w-2xl / 3xl / 4xl / 7xl. Names map in that order. Reads always return the class. |
| status | draft or active only, so a promotion cannot be deleted or resurrected here. Publishing needs a future end_date (stored or in the same request) or returns 422 VALIDATION_ERROR. Re-sending active is a no-op. Unpublishing keeps entries and hides the page from visitors. |
| entry_methods | Array, max 30. Replaces the whole array. See below. |
Media fields. image_video (hero), secondary_image, third_image, fourth_image, fifth_image, background_image and og_image take a URL or null to clear. Each is ≤500 chars on media.tokei.io or res.cloudinary.com over https, which is what public_url from POST /api/v1/media returns. Upload there, then PATCH it in. og_image is the social-share preview. It falls back to the account image when null, and a GIF or video is dropped, so use JPG, PNG or WEBP.
entry_methods. There is no per-row patch, so GET the promotion, edit the array and PATCH the full list back. [] clears every method and omitting the field leaves it untouched. Each element is one of two shapes.
- Action row:
{ id?, actionType, label, points?, config?, requireVerification? }, whereactionTypeis one of the 28 writable types from GET /api/v1/actions/catalog. - Link row (Custom Link):
{ id?, label, points?, link, actionsRequired? }, with noactionType. A plain http(s) button to any URL.
- actionsRequired (0–20) hides the button until the entrant completes that many other actions. Action rows accept it only for
tokei_cross_promote. Elsewhere it is a promotion setting keyed byactionTypeand is dropped if sent. - Stripped keys.
icon,config.typeand the ten legacy<platform>Configduplicates (such asproductHuntConfig) are removed silently, so a live row can be PATCHed back unmodified. The server derivesiconandconfig.typefromactionType. - Row id. Optional, 1–64 chars of
[A-Za-z0-9_-]. It gives a row a stable identity so a later PATCH can target one of several rows with the sameactionType. Stored rows always have one, and a row sent without one gets one. A repeated id returns 422 VALIDATION_ERROR. - Changing a target keeps credit. A row stays the same action while it keeps its
id. Entrants who completed it before an edit keep their credit and are not paid again. The server stores the old target in read-onlypreviousTargetsandtargetSuffixand ignores values you send for them. To make everyone do it again, send the row without anid. - Per-type cap. Up to 5 rows may share an
actionType, only fortwitter_follow,instagram_followandfacebook_visit_page. Every other type allows 1. Exceeding the cap returns 422 VALIDATION_ERROR. - points is 0–100.
0means unset and the renderer uses the type's default.GETreturns the effective points (anysettings.<actionType>_entry_valueoverride applied), so a GET → PATCH round-trip stores that effective value. Product Hunt and Steam rows always display the platform default, whatever is stored. - Reserved label. A link label starting with
"Visit Our "(capital O) is the promotion's campaign-visit button, owned bycampaign_url. The dashboard rewrites it, so a PATCHed value reverts when the owner next opens the editor. Lowercase"Visit our …"is not reserved. Every other link row is a clickable button that opens itslinkand credits itspointson all four templates.
fetch("https://tokei.io/api/v1/contests/CONTEST_ID", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
title: "Win a Nintendo Switch 2",
description: "Enter free before the deadline.",
end_date: "2026-08-31T23:59:59Z",
prizes: [{ name: "Nintendo Switch 2", winners: 1, value: 449, currency: "USD" }],
}),
});200 returns the full updated promotion object:
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Win a Nintendo Switch 2",
"description": "Enter free before the deadline.",
"status": "active",
"mode": "competition",
"type": "promotion",
"contest_url": "win-nintendo-switch",
"public_url": "https://tokei.io/contest/win-nintendo-switch",
"end_date": "2026-08-31T23:59:59Z",
"days_left": 42,
"total_entries": 847,
"total_points_awarded": 4235,
"settings": { "template": "basic-new", "color": "#6366F1" },
"prizes": [{ "name": "Nintendo Switch 2", "winners": 1, "value": 449, "currency": "USD" }],
"reward_thresholds": [],
"updated_at": "2026-07-20T09:00:00Z"
}
}Templates
List Templates
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/templates | List the platform's named starting points |
Templates are platform content, not the caller's, so this listing is not ownership-scoped — every authenticated key sees the same list. No parameters, no pagination. Clone one by slug with POST /api/v1/promotions {"template": "<slug>"}. Each template: id, slug (pass as template to clone it), name, skin (page skin — basic-new, showcase, or future, same vocabulary as PATCH's template field), and entry_method_count. A count of 0 does not always mean a stub — three templates report 0 because their action lives on the page rather than in entry_methods: secret-codes (the secret code), survey-system (a survey and a photo upload) and collect-email-list (the sign-up form). A secret-codes clone keeps the code copy but switches the input off and copies no codes — the owner uploads codes in the dashboard, then turns it on.
{
"success": true,
"data": [
{
"id": "0e4de06c-8915-4e1c-ba3f-48f6c9a098f2",
"slug": "collect-email-list",
"name": "Collect email list — registration-first opt-in subscriber page",
"skin": "future",
"entry_method_count": 0
},
{
"id": "06743256-2e8e-4ede-a431-f17866fae1f6",
"slug": "competition-starter",
"name": "Starter — Gleam-style competition giveaway (X, Instagram, TikTok, Facebook entries)",
"skin": "basic-new",
"entry_method_count": 6
},
{
"id": "c0cc71a0-1ff5-46ab-8a23-c801aec30337",
"slug": "instagram-engagement",
"name": "Instagram engagement — follow & share photo giveaway",
"skin": "showcase",
"entry_method_count": 3
},
{
"id": "6ca0bdbc-23d8-4c79-a894-857eea485fbe",
"slug": "secret-codes",
"name": "Secret Code — unlock entries with a code (QR codes, receipts, printed inserts, events)",
"skin": "basic-new",
"entry_method_count": 0
},
{
"id": "88fde228-8baf-4b31-9b0e-cc243b3cc83d",
"slug": "steam-promotion",
"name": "A futuristic Steam template for Adding to Steam Wishlists and Playing Steam Games.",
"skin": "future",
"entry_method_count": 6
}
]
}An excerpt — 15 templates are live today, sorted by slug. The other ten: discord-community, facebook-promotion, family-friends, prelaunch-vips, product-hunt, survey-system, tiktok-growth, twitch-growth, x-followers, youtube-contest. Always call the endpoint rather than hardcoding this list.
Actions
Entry-Action Catalog
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/actions/catalog | Every entry-action type Tokei supports |
The catalog is static platform data, not the caller's, so — like GET /api/v1/templates — it is not ownership-scoped: every authenticated key sees the same catalog, and there is no database read at all. Optional ?type= returns just that one action type's entry (a value matching no action type is a 400 BAD_REQUEST listing every valid value); omitting it returns all 40 action types keyed by actionType. Only 28 of the 40 are writable as an entry_methods row (isEntryMethodRow: true) — the other 12 are enabled through a contest setting or a dedicated route instead, and needs documents any deployment prerequisite (most often participant OAuth) an entry method of that type requires.
This is the authoritative per-type reference for PATCH /api/v1/contests/:contestId's entry_methods — each entry's fields array is exactly what that endpoint accepts under config. Fields sharing a group are ordered renderer-read-first: write the first field of the group. The rest are claim-validator aliases that satisfy validation but render a dead button on their own — each carries a note saying so.
tokei-agent actions:catalog --type twitter_follow
# or: fetch("https://tokei.io/api/v1/actions/catalog?type=twitter_follow", {
# headers: { Authorization: `Bearer ${API_KEY}` },
# });{
"success": true,
"data": {
"label": "Follow on X",
"description": "Follow an account on X (Twitter)",
"requiresAuth": false,
"defaultPoints": 3,
"platform": "twitter",
"trustBased": false,
"apiVerification": false,
"manualVerifiable": true,
"entryValueSettingKey": "twitter_follow_entry_value",
"isEntryMethodRow": true,
"fields": [
{ "key": "username", "type": "string", "required": true,
"note": "X/Twitter handle. Checked for presence only — no format validation." }
],
"needs": null
}
}Events
Webhook Event Catalog
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/events/catalog | Every webhook event Tokei's delivery engine understands |
The catalog is static platform data, not the caller's, so — like GET /api/v1/actions/catalog — it is not ownership-scoped: every authenticated key sees the same catalog, and there is no database read at all. Optional ?type= returns just that one event's entry (a value matching no event name is a 400 BAD_REQUEST listing every valid value); omitting it returns all 5 events keyed by event name. Each entry carries description, payloadSchema (the shape of the delivered data field — see Webhooks for the outer envelope), emitSites, and subscribable (true for all 5 this stage).
tokei-agent events:catalog --type winner.selected
# or: fetch("https://tokei.io/api/v1/events/catalog?type=winner.selected", {
# headers: { Authorization: `Bearer ${API_KEY}` },
# });Sample response — the winners item schema is abbreviated here for readability; the real response spells out all eight per-winner properties (id, contest_user_id, email, full_name, points, prize_tier, prize_description, prize_value) with its own required list. Call the endpoint for the authoritative shape.
{
"success": true,
"data": {
"description": "Winner selection was finalized and persisted for a contest.",
"payloadSchema": {
"type": "object",
"properties": {
"contest_id": { "type": "string" },
"selection_run_id": { "type": "string" },
"winners": { "type": "array", "items": { "type": "object" } }
},
"required": ["contest_id", "selection_run_id", "winners"]
},
"emitSites": ["src/app/api/promotion/[contestId]/selection/route.ts"],
"subscribable": true
}
}Promotions
Create a Promotion (Clone)
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/promotions | Clone a promotion you own, with copy overrides (write scope) |
Creation is clone-based: keep one polished master promotion per page shape, then let the API copy its template, theme, entry methods and settings verbatim while you override the marketing copy. Body fields: source_promotion_id (optional — a promotion you own) or template (optional — a slug from GET /api/v1/templates, e.g. "product-hunt"); they're alternatives — sending both is a 422, a template slug matching no template is a 404, and omitting both clones the platform starter template. title (required — sets the dashboard name AND the public headline), description, prize (sets the prize — renames the first or creates one), end_date (defaults to the source's original duration), campaign_url, image_url (hero image — a Supabase storage or Cloudinary URL), status (draft default, active goes live immediately), and idempotency_key (reuse returns 409 CONFLICT with the existing promotion's id). Limited to 20 API-created promotions per account per UTC day.
fetch("https://tokei.io/api/v1/promotions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
source_promotion_id: "MASTER_PROMOTION_ID",
title: "Win a Nintendo Switch 2 — launch week special",
prize: "Nintendo Switch 2 + 3 games",
end_date: "2026-07-31T23:59:59Z",
status: "active",
idempotency_key: "n8n-run-8823",
}),
});201 returns the live page URL directly — no follow-up lookup needed:
{
"success": true,
"data": {
"id": "9a1b2c3d-e5f6-7890-abcd-ef1234567890",
"title": "Win a Nintendo Switch 2 — launch week special",
"status": "active",
"mode": "competition",
"type": "promotion",
"contest_url": "x8Kp2m",
"public_url": "https://tokei.io/contest/x8Kp2m",
"edit_url": "https://tokei.io/dashboard/promotion/edit?id=9a1b2c3d-…",
"source_promotion_id": "MASTER_PROMOTION_ID",
"end_date": "2026-07-31T23:59:59Z",
"created_at": "2026-07-02T14:00:00Z"
}
}How automation uses this
The common flow is “a new product drops → launch a campaign”. Your automation (n8n, Zapier, Make) fires on a trigger, generates the copy and a hero image, then makes a single call. Omit source_promotion_id and the platform starter template is cloned for you — so a brand-new account with nothing to clone still works.

- Trigger fires (e.g. a new product in your store, an RSS item).
- AI drafts the
title,descriptionandprize. - Upload the image to
POST /api/v1/media. - One POST with
status: "active"creates a live promotion. - The 201 response's
public_urlis posted to your channels — no follow-up lookup.
// No source_promotion_id → clones the platform starter template
fetch("https://tokei.io/api/v1/promotions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
title: "Win a Nintendo Switch 2",
image_url:
"https://media.tokei.io/media/switch.jpg",
prize: "Nintendo Switch 2",
description: "<p><b>Launch-week giveaway — enter free.</b></p>",
status: "active",
}),
});201 — the clone inherits the template's design (template, theme, entry methods, subheading) and applies your copy and image:
{
"success": true,
"data": {
"id": "0d1e2f3a-4b5c-6d7e-8f90-1a2b3c4d5e6f",
"title": "Win a Nintendo Switch 2",
"status": "active",
"type": "promotion",
"contest_url": "x8Kp2m",
"public_url": "https://tokei.io/contest/x8Kp2m",
"edit_url": "https://tokei.io/dashboard/promotion/edit?id=0d1e2f3a-…",
"source_promotion_id": "STARTER_TEMPLATE_ID",
"created_at": "2026-07-03T14:00:00Z"
}
}
Media
Upload Media
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/media | Upload an image or video (write scope) |
Requires the write scope. Send the file as multipart/form-data with a file part; the object is stored, so success is 201. Feed the returned public_url into one of the seven media fields on PATCH /api/v1/contests/:contestId (image_video, secondary_image, third_image, fourth_image, fifth_image, background_image, og_image) — it is guaranteed to pass their host allowlist, so you can upload then PATCH it straight in.
The file part
| Part | Required | Description |
|---|---|---|
| file | Yes | The bytes, with a filename and a content type. The filename is echoed back but never stored — the object's stored name is always a server-generated UUID, so path traversal in it can only be echoed, never acted on. |
Accepted content types: image/jpeg, image/png, image/gif, image/webp, video/mp4, video/webm, video/quicktime. application/pdf is not accepted — this endpoint is images and video only. The part's declared type is the sole source of the stored extension. For images and video/mp4, the bytes are checked against it — a file whose leading bytes contradict the declared type is a 422, not a silently mislabelled object. video/webm and video/quicktime are accepted but not byte-checked; only the declared type decides those two.
Size: 100 bytes to 4 MB. The 4 MB cap applies to video as well as images — thin for video, so plan accordingly. An oversize file is a 413 in Tokei's own error format.
Abuse guard: max 50 uploads per account per UTC day — 429 RATE_LIMIT_EXCEEDED with Retry-After. The cap is checked after validation, so a rejected request does not burn quota.
Upload
const form = new FormData();
form.append("file", fileBlob, "hero.jpg");
const uploaded = await fetch("https://tokei.io/api/v1/media", {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}` },
body: form, // do NOT set Content-Type yourself — the boundary must match
}).then((r) => r.json());201 returns the stored object:
{
"success": true,
"data": {
"bucket": "tokei-media",
"path": "api-uploads/usr_a1b2c3d4/0f8fad5b-d9cb-469f-a165-70867728950e.jpg",
"public_url": "https://media.tokei.io/api-uploads/usr_a1b2c3d4/0f8fad5b-d9cb-469f-a165-70867728950e.jpg",
"content_type": "image/jpeg",
"filename": "hero.jpg",
"size_bytes": 482113,
"backend": "r2"
}
}backend names the store the object landed in, and public_url is always absolute — read the URL from the response rather than assembling one from bucket and path, because the host is migrating.
Errors specific to this endpoint
| Status | Code | Note |
|---|---|---|
| 410 | GONE | The retired JSON signed-upload ticket shape. Send the file as multipart/form-data instead. |
| 413 | PAYLOAD_TOO_LARGE | File over 4 MB. |
| 422 | VALIDATION_ERROR | Missing file part, a content type outside the seven accepted values, a file under 100 bytes, or bytes that do not match the declared type. |
| 429 | RATE_LIMIT_EXCEEDED | 50 uploads/day per account. |
| 500 | INTERNAL_ERROR | The store rejected the write. |
Then PATCH it onto a promotion
public_url is live and already allowlisted:
await fetch(`https://tokei.io/api/v1/contests/${CONTEST_ID}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({ image_video: uploaded.data.public_url }),
});Retired: the signed-upload ticket
Sending application/json with filename, content_type and size_bytes used to return a two-step signed upload ticket. It minted a storage-specific token that the current media host has no equivalent for, so it now answers 410 GONE. Use the multipart form above.
Entries
List & Create Entries
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/contests/:contestId/entries | List entries (filter with ?email=) |
| POST | /api/v1/contests/:contestId/entries | Create an entry (write scope) |
POST creates a participant programmatically. Body fields: email (required), name, action_type (default api_import), points, value, and metadata (custom key-value pairs). Creating a duplicate (same email and promotion) returns 409 CONFLICT.
fetch("https://tokei.io/api/v1/contests/CONTEST_ID/entries", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
email: "newuser@example.com",
name: "Alice Johnson",
action_type: "email_signup",
points: 5,
value: "Shopify Order #98765",
metadata: { order_total: "49.99", source: "shopify" },
}),
});Successful creation returns 201 with the new entry:
{
"success": true,
"data": {
"id": "e5f6a7b8-c9d0-1234-ef01-345678901234",
"email": "newuser@example.com",
"full_name": "Alice Johnson",
"entry_points": 5,
"action_type": "email_signup",
"referral_code": "qR5tUv",
"created_at": "2026-02-17T12:00:00Z"
}
}Analytics
GET /api/v1/contests/:contestId/analytics
Aggregated promotion analytics: totals, action breakdown, daily entry counts, top countries, and referral stats.
{
"success": true,
"data": {
"contest_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"total_entries": 847,
"unique_participants": 312,
"total_points_awarded": 4235,
"actions_breakdown": { "email_signup": 312, "twitter_follow": 198 },
"daily_entries": [ { "date": "2026-02-10", "count": 45 } ],
"top_countries": [ { "country_code": "US", "country_name": "United States", "count": 156 } ],
"referral_stats": {
"total_referral_clicks": 234,
"total_referral_conversions": 45,
"conversion_rate": 0.192
}
}
}Leaderboard
GET /api/v1/contests/:contestId/leaderboard
Participants ranked by points (descending), paginated.
{
"success": true,
"data": [
{
"rank": 1,
"full_name": "John Doe",
"email": "john@example.com",
"entry_points": 85,
"referral_code": "mN3pQr",
"country_code": "GB"
}
],
"pagination": { "page": 1, "per_page": 10, "total_pages": 32, "total_count": 312 }
}Referrals
GET /api/v1/contests/:contestId/referrals
Top referrers, ranked by converted referrals then total referrals, paginated — plus whole-promotion totals. Every participant is issued a referral code, so entrants who have not actually referred anyone are left out. converted_referrals counts referred people who went on to complete an entry action other than sharing. bonus_points_earned is a count of bonus entries, not a points total.
{
"success": true,
"data": [
{
"rank": 1,
"referrer_id": "88941e21-0229-4d61-b639-ddbcae3b78ef",
"referral_code": "4IXgZbGo",
"full_name": "John Doe",
"email": "john@example.com",
"total_referrals": 3,
"converted_referrals": 3,
"bonus_points_earned": 3
}
],
"totals": {
"total_referrers": 5,
"total_referrals": 8,
"total_clicks": 279,
"converted_clicks": 7,
"click_conversion_rate": 2.5
},
"pagination": { "page": 1, "per_page": 25, "total_pages": 1, "total_count": 5 }
}Winners
GET /api/v1/contests/:contestId/winners
Read-only, no query parameters. Selection runs for this promotion, newest first, each with its persisted winners nested and the same entrant identity fields the dashboard already shows the owner. Finalizing a selection stays dashboard-only (human-approval policy) — this endpoint is how an agent looks back at what a run actually selected, not how it draws one. No pagination: contest-scale run and winner counts make a fixed cap (100 runs) sufficient headroom rather than a pagination story.
{
"success": true,
"data": [
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"created_at": "2026-08-01T09:00:00Z",
"seed": "8f2a1c",
"algorithm_version": "v1",
"status": "finalized",
"requested_by_email": "owner@example.com",
"finalized_at": "2026-08-01T09:00:05Z",
"total_candidates": 312,
"total_winners_selected": 1,
"winners_count": 1,
"winners": [
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"contest_user_id": "88941e21-0229-4d61-b639-ddbcae3b78ef",
"email": "john@example.com",
"full_name": "John Doe",
"entry_points": 85,
"created_at": "2026-07-15T10:00:00Z",
"country_name": "United Kingdom",
"city": "London",
"prize_tier": "grand_prize",
"prize_description": "AirPods Pro",
"prize_value": 249,
"selected_at": "2026-08-01T09:00:05Z",
"notified_at": null,
"notification_method": null,
"verified": false,
"claimed_at": null
}
]
}
]
}Surveys
GET /api/v1/contests/:contestId/survey-responses
Survey responses collected from gamified promotions, newest first, paginated.
{
"success": true,
"data": [
{
"id": "f6a7b8c9-d0e1-2345-f012-456789012345",
"contest_user_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"response": "I love gaming and would play every day!",
"question_index": 0,
"points_awarded": 3,
"is_completed": true,
"created_at": "2026-02-12T16:45:00Z"
}
],
"pagination": { "page": 1, "per_page": 25, "total_pages": 5, "total_count": 109 }
}Webhooks
Webhooks
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/webhooks | List webhook subscriptions |
| POST | /api/v1/webhooks | Create a webhook |
| DELETE | /api/v1/webhooks/:webhookId | Delete a webhook |
Available events
| Event | Trigger |
|---|---|
| entry.created | A participant completed an entry action (email, OAuth, or a public-API entry write). |
| winner.selected | Winner selection was finalized and persisted for a contest. |
| contest.ended | A contest's end date passed, observed by the contest-lifecycle worker (polls every 5 minutes). |
| daily_bonus.claimed | A participant claimed their daily bonus entry for a contest. |
| referral.converted | A referred participant joined a contest (and confirmed their email, on promotions that require it) and the referrer's conversion bonus was awarded. |
All 5 events are emitted and subscribable through this API. See GET /api/v1/events/catalog (also events:catalog in the CLI/MCP) for each event's full payload shape, live. Per-contest creator webhooks, configured from the Tokei dashboard rather than this API, subscribe to a fixed, curated trio — entry.created, winner.selected, contest.ended — with no event-picker UI; daily_bonus.claimed and referral.converted stay developer-API-only, reachable only through POST /api/v1/webhooks above.
Create a webhook
Provide an HTTPS url and at least one event. The response includes the signing secret — shown only once:
curl -X POST "https://tokei.io/api/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourserver.com/webhooks/tokei", "events": ["entry.created"]}'
{
"success": true,
"data": {
"id": "w1h2k3i4-d5e6-7890-abcd-ef1234567890",
"url": "https://yourserver.com/webhooks/tokei",
"events": ["entry.created"],
"secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"is_active": true,
"created_at": "2026-02-17T12:00:00Z"
}
}Payload format
Every delivery shares the same envelope — event, timestamp, data — with data's shape varying per event. entry.created below is one example; GET /api/v1/events/catalog is the authoritative, per-event reference for all 5.
{
"event": "entry.created",
"timestamp": "2026-02-17T12:00:00Z",
"data": {
"contest_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"entry_id": "e5f6a7b8-c9d0-1234-ef01-345678901234",
"email": "newuser@example.com",
"full_name": "Alice Johnson",
"first_name": "Alice",
"last_name": "Johnson",
"entry_source": "email_signup",
"points": 5,
"action_type": "email_signup"
}
}Signature verification
Every delivery is signed with HMAC-SHA256 in the X-TOKEI-Signature header. Verify it against the raw request body using your webhook secret:
const crypto = require("crypto");
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}Delivery behavior
- Your endpoint must respond with a 2xx status within 10 seconds.
- Failed deliveries are retried with backoff: 5s, 30s, then 5 minutes.
- Subscriptions are auto-disabled after 10 consecutive failures.
- Webhook URLs must use HTTPS; redirects are treated as failures.
Reference
Action Types
| Action type | Platform | Description |
|---|---|---|
| email_signup | Enter with email | |
| twitter_follow | Twitter/X | Follow an account |
| twitter_like | Twitter/X | Like a tweet |
| twitter_retweet | Twitter/X | Retweet a tweet |
| twitter_view_post | Twitter/X | View a post on X |
| twitter_tweet | Twitter/X | Post a tweet on X |
| instagram_follow | Follow an account | |
| instagram_view_post | View a post | |
| instagram_share_photo | Share a photo | |
| tiktok_follow | TikTok | Follow an account |
| tiktok_watch | TikTok | Watch a video |
| facebook_visit_page | Visit a page | |
| facebook_view_post | View a post | |
| facebook_join_group | Join a group | |
| discord_join | Discord | Join a server |
| twitch_follow | Twitch | Follow a channel |
| twitch_subscribe | Twitch | Subscribe to a channel |
| youtube_visit_channel | YouTube | Visit a channel |
| linkedin_follow | Follow a profile | |
| linkedin_company_follow | Follow a company page | |
| linkedin_share | Share campaign content | |
| linkedin_post | Create a post | |
| steam_wishlist | Steam | Add a game to wishlist |
| steam_play_game | Steam | Play or own a game |
| steam_join_group | Steam | Join a group |
| tokei_cross_promote | Tokei | Enter another Tokei promotion |
| app_download | App | Open an app's store page (installs are not verified) |
| producthunt_follow | Product Hunt | Follow on Product Hunt |
| producthunt_vote | Product Hunt | Upvote on Product Hunt |
| visit_website | Web | Visit a URL |
| referral_share | Referral | Share referral link |
| referral_conversion_bonus | Referral | Bonus for referred signups |
| user_upload | Upload | Upload an image |
| daily_bonus | Gamification | Claim daily bonus |
| secret_code | Gamification | Read-only — Secret Code claim, enabled by a promotion setting |
| custom_action | Custom | Read-only — legacy type, no longer written |
| api_import | API | Entry created via API |
| oauth_login | Auth | Read-only — appears in analytics, not settable via POST |
| social_share | Web | Read-only — generic share tracked automatically |
| survey | Gamification | Read-only — survey response, not settable via POST |
| quick_question | Gamification | Read-only — Multiple Choice answer, not settable via POST |
This table is a static summary. GET /api/v1/actions/catalog (also actions:catalog in the CLI/MCP) is the live, authoritative per-type reference — platform, default points, per-type config fields, and any deployment prerequisite.
Examples
Integration Examples
Shopify: award an entry on purchase
async function handleShopifyOrder(order) {
const CONTEST_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
const response = await fetch(
`https://tokei.io/api/v1/contests/${CONTEST_ID}/entries`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
email: order.customer.email,
name: `${order.customer.first_name} ${order.customer.last_name}`,
points: 10,
value: `Shopify Order #${order.order_number}`,
metadata: { order_total: order.total_price, source: "shopify" },
}),
}
);
const result = await response.json();
console.log("Entry created:", result.data.id);
}Python: list active promotions
import requests
API_KEY = "tokei_k_..."
response = requests.get(
"https://tokei.io/api/v1/contests",
params={"status": "active"},
headers={"Authorization": f"Bearer {API_KEY}"}
)
for promotion in response.json()["data"]:
print(f"{promotion['title']} - {promotion['total_entries']} entry actions")Full documentation index: tokei.io/llms.txt