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

  1. Log in to your Tokei dashboard and go to Dashboard > Settings > API.
  2. Click Create New Key, name it, and copy the full key — it is shown only once.
  3. 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 saysDashboard / people sayNotes
contestpage, campaign, promotionThe core object. contestId in a path is the page's id.
promotionpage, campaignThe same object again — POST /promotions creates it, and it reads back under /contests/:contestId.
entrysignup, subscriber, entrantOne person joining a page.
titleheadlineThe visible page headline — not the internal dashboard name (campaign_name, read-only).
settings.templateskin, designbasic-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

StatusCodeDescription
200OKRequest succeeded.
201CREATEDResource created.
400BAD_REQUESTInvalid parameters.
401UNAUTHORIZEDMissing, invalid, or revoked API key.
403FORBIDDENValid key but insufficient plan or missing scope.
404NOT_FOUNDPromotion or resource not found.
409CONFLICTDuplicate entry (email already entered this promotion) or reused idempotency_key.
409WINNERS_ALREADY_SELECTEDAn end_date write — setting or clearing — on a promotion whose winners have already been drawn. The deadline is frozen once a draw has happened.
413PAYLOAD_TOO_LARGERequest body exceeds the 10KB limit (PATCH /api/v1/contests/:contestId alone is raised to 64KB, to fit a full entry_methods array).
422VALIDATION_ERRORRequest body failed validation — see the details array.
429RATE_LIMIT_EXCEEDEDToo many requests — see the Retry-After header.
500INTERNAL_ERRORServer 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

PlanRead / minWrite / minDaily cap
Subscriber603010,000
Lifetime1206050,000

Every response includes rate-limit headers; 429 responses also include Retry-After:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1708200000

The 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 parameterDefaultDescription
page_typeall fourComma-separated subset of contest, levelup, promote, discover. An unrecognised value is a 400, not an empty list.
limit50Rows per response, 1–100.
offset0Rows 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 --help

Requires Node 22.12+. Published on npm; the agent landing page is at tokei.io/agent.

Configuration

VariableRequiredMeaning
TOKEI_API_KEYYesSent as Authorization: Bearer <key>. Create one at Dashboard > Settings > API.
TOKEI_API_URLNoBase 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 codeMeaning
0Success (HTTP 2xx).
1API or network error. The JSON error body is still printed on stdout; pure network failures print {"ok": false, "error": {"type": "network_error", ...}}.
2Usage 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)

CommandEndpointDoes
meGET /meVerify the key; account, plan, API usage
pages:listGET /contestsList pages — --status, --mode, --page, --per-page
pages:get <contestId>GET /contests/:contestIdOne page in full (prizes, reward tiers, media, public URL)
stats <contestId>GET /contests/:contestId/analyticsAggregated analytics
leaderboard <contestId>GET /contests/:contestId/leaderboardParticipants ranked by points
referrals:top <contestId>GET /contests/:contestId/referralsTop referrers, ranked by conversions
winners:list <contestId>GET /contests/:contestId/winnersSelection-run history, newest first, with each run's winners
entries:list <contestId>GET /contests/:contestId/entriesSignups — filter with --email
surveys:list <contestId>GET /contests/:contestId/survey-responsesSurvey responses
webhooks:listGET /webhooksList webhook subscriptions (no write scope needed)
templates:listGET /templatesNamed starting points, for pages:clone --template
actions:catalogGET /actions/catalogEvery entry-action type Tokei supports — filter with --type
events:catalogGET /events/catalogEvery webhook event Tokei's delivery engine understands — filter with --type

Write (read+write key)

CommandEndpointDoes
pages:clonePOST /promotionsCreate a page from one you own, a named template, or the starter. 20/day cap
media:upload <file>POST /media, then PUT to storageUpload an image or video and get back a public_url. ≤4MB per file (video too)
pages:update <contestId>PATCH /contests/:contestIdUpdate title, description, dates, prizes, reward tiers, appearance and media
pages:publish <contestId>PATCH /contests/:contestIdSugar for {"status": "active"} — needs a future end_date
pages:unpublish <contestId>PATCH /contests/:contestIdSugar for {"status": "draft"} — blocks new signups, but the page still renders publicly
entries:create <contestId>POST /contests/:contestId/entriesAdd a signup
webhooks:createPOST /webhooksSubscribe an HTTPS endpoint to one or more of the 5 events — the whsec_ secret is shown once
webhooks:delete <webhookId>DELETE /webhooks/:webhookIdRemove 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 mcp

Or 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:list becomes pages_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, with isError set 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.

  1. Check the key and plan first. API access requires an active subscription or lifetime plan — trial accounts get 403 on every call, so a whole workflow can fail on its first request for a reason no other error explains. Note that me reports the plan but not the key's scope: you discover a read-only key by getting 403 FORBIDDEN on your first write.
  2. Media must be uploaded through Tokei first. The seven media fields on PATCH /api/v1/contests/:contestId are guarded by a host allowlist, so local paths and third-party URLs are rejected. Upload, then patch the public_url you get back — see Upload Media.
  3. List fields replace wholesale. prizes (max 50) and reward_thresholds (max 50) are not merged — whatever array you send becomes the entire list. Always read, modify, then write the complete list back.
  4. 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 status separates 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.

MethodEndpointDescription
GET/api/v1/contestsList all your promotions
GET/api/v1/contests/:contestIdRetrieve 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

MethodEndpointDescription
PATCH/api/v1/contests/:contestIdUpdate 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.

FieldAccepts
title1–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_dateISO 8601 or null. A future date pauses new entries until then.
end_dateFuture 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.
prizesUp to 20 of { name, winners, value?, currency? }. Replaces the list.
reward_thresholdsUp to 50 of { id, points, rewardType, rewardDescription, rewardDetails?, isEnabled }. Replaces the list and recomputes the VIP threshold from the cheapest enabled vip_status tier.
templatebasic-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_enabledBoolean. A creator toggle; visitors' color scheme is ignored.
primary_colorHex only (3, 4, 6 or 8 digits, e.g. #7d78c6), or null for the template default. Other CSS color formats are rejected.
card_widthnarrow, medium, wide, xl, or the stored class max-w-2xl / 3xl / 4xl / 7xl. Names map in that order. Reads always return the class.
statusdraft 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_methodsArray, 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? }, where actionType is one of the 28 writable types from GET /api/v1/actions/catalog.
  • Link row (Custom Link): { id?, label, points?, link, actionsRequired? }, with no actionType. 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 by actionType and is dropped if sent.
  • Stripped keys. icon, config.type and the ten legacy <platform>Config duplicates (such as productHuntConfig) are removed silently, so a live row can be PATCHed back unmodified. The server derives icon and config.type from actionType.
  • 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 same actionType. 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-only previousTargets and targetSuffix and ignores values you send for them. To make everyone do it again, send the row without an id.
  • Per-type cap. Up to 5 rows may share an actionType, only for twitter_follow, instagram_follow and facebook_visit_page. Every other type allows 1. Exceeding the cap returns 422 VALIDATION_ERROR.
  • points is 0–100. 0 means unset and the renderer uses the type's default. GET returns the effective points (any settings.<actionType>_entry_value override 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 by campaign_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 its link and credits its points on 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

MethodEndpointDescription
GET/api/v1/templatesList 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

MethodEndpointDescription
GET/api/v1/actions/catalogEvery 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

MethodEndpointDescription
GET/api/v1/events/catalogEvery 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)

MethodEndpointDescription
POST/api/v1/promotionsClone 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.

Automation flow: a product-launch trigger generates campaign copy and a hero image, then a single POST to /api/v1/promotions returns a live public page URL.
  1. Trigger fires (e.g. a new product in your store, an RSS item).
  2. AI drafts the title, description and prize.
  3. Upload the image to POST /api/v1/media.
  4. One POST with status: "active" creates a live promotion.
  5. The 201 response's public_url is 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"
  }
}
Anatomy of a generated promotion: the title becomes the public heading, image_url becomes the hero image, prize sets the prize, and the subheading is inherited from the starter template.

Media

Upload Media

MethodEndpointDescription
POST/api/v1/mediaUpload 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

PartRequiredDescription
fileYesThe 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

StatusCodeNote
410GONEThe retired JSON signed-upload ticket shape. Send the file as multipart/form-data instead.
413PAYLOAD_TOO_LARGEFile over 4 MB.
422VALIDATION_ERRORMissing file part, a content type outside the seven accepted values, a file under 100 bytes, or bytes that do not match the declared type.
429RATE_LIMIT_EXCEEDED50 uploads/day per account.
500INTERNAL_ERRORThe 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

MethodEndpointDescription
GET/api/v1/contests/:contestId/entriesList entries (filter with ?email=)
POST/api/v1/contests/:contestId/entriesCreate 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

MethodEndpointDescription
GET/api/v1/webhooksList webhook subscriptions
POST/api/v1/webhooksCreate a webhook
DELETE/api/v1/webhooks/:webhookIdDelete a webhook

Available events

EventTrigger
entry.createdA participant completed an entry action (email, OAuth, or a public-API entry write).
winner.selectedWinner selection was finalized and persisted for a contest.
contest.endedA contest's end date passed, observed by the contest-lifecycle worker (polls every 5 minutes).
daily_bonus.claimedA participant claimed their daily bonus entry for a contest.
referral.convertedA 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 typePlatformDescription
email_signupEmailEnter with email
twitter_followTwitter/XFollow an account
twitter_likeTwitter/XLike a tweet
twitter_retweetTwitter/XRetweet a tweet
twitter_view_postTwitter/XView a post on X
twitter_tweetTwitter/XPost a tweet on X
instagram_followInstagramFollow an account
instagram_view_postInstagramView a post
instagram_share_photoInstagramShare a photo
tiktok_followTikTokFollow an account
tiktok_watchTikTokWatch a video
facebook_visit_pageFacebookVisit a page
facebook_view_postFacebookView a post
facebook_join_groupFacebookJoin a group
discord_joinDiscordJoin a server
twitch_followTwitchFollow a channel
twitch_subscribeTwitchSubscribe to a channel
youtube_visit_channelYouTubeVisit a channel
linkedin_followLinkedInFollow a profile
linkedin_company_followLinkedInFollow a company page
linkedin_shareLinkedInShare campaign content
linkedin_postLinkedInCreate a post
steam_wishlistSteamAdd a game to wishlist
steam_play_gameSteamPlay or own a game
steam_join_groupSteamJoin a group
tokei_cross_promoteTokeiEnter another Tokei promotion
app_downloadAppOpen an app's store page (installs are not verified)
producthunt_followProduct HuntFollow on Product Hunt
producthunt_voteProduct HuntUpvote on Product Hunt
visit_websiteWebVisit a URL
referral_shareReferralShare referral link
referral_conversion_bonusReferralBonus for referred signups
user_uploadUploadUpload an image
daily_bonusGamificationClaim daily bonus
secret_codeGamificationRead-only — Secret Code claim, enabled by a promotion setting
custom_actionCustomRead-only — legacy type, no longer written
api_importAPIEntry created via API
oauth_loginAuthRead-only — appears in analytics, not settable via POST
social_shareWebRead-only — generic share tracked automatically
surveyGamificationRead-only — survey response, not settable via POST
quick_questionGamificationRead-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