How to use the fair.ninja API: MCP, tokens and webhooks
Connect Claude or ChatGPT to your tasks and timer through MCP, call the REST API with a personal token, and receive signed webhooks. What each costs.
On this page
Tomato keeps your tasks and focus timer on your own devices. With an account, a copy lives on our server, and that copy is what the API reaches. This guide covers the three ways in, from the least code to the most:
- MCP: an AI assistant works with your tasks and timer. No code.
- REST with a personal API token: your own scripts read your data and send changes.
- Webhooks: we call your server when something changes, with a signature you can check.
All three are managed in the account portal under Integrations. Everything below applies to accounts with server sync. The apps themselves work without an account, and nothing in this guide is needed to use them.
Connect an AI assistant through MCP
MCP (Model Context Protocol) is an open protocol that lets an AI assistant use outside tools. Our server URL is:
https://api.fair.ninja/mcp
Sign in with OAuth
This is the way for assistants that add a remote server by URL. There is no client id, secret or key to copy:
- Add a custom connector in your assistant and paste the server URL. Any assistant that can add a remote MCP server by URL works; where that option lives is in your assistant’s own help.
- Click connect. A fair.ninja sign-in page opens.
- Sign in, then read the consent page. It names the app and shows one checkbox per permission.
- Untick what the assistant should not have, and allow.
| Permission | What it allows |
|---|---|
tasks:read |
Read tasks and collections |
tasks:write |
Create and change tasks and collections (includes reading them) |
time:read |
Read the timer and time reports |
time:write |
Control the timer and add time (includes reading it) |
wallet:read |
Read your balance and this month’s usage |
private:read |
Read task notes |
Without private:read, notes are left out of every answer. The grant reaches every space you belong to. To end it, open Connected apps in Integrations.
Or use a personal MCP token
Clients that let you set a request header can use a personal token instead. In Integrations, open AI assistants, create a token, choose its permissions, and copy the secret. It starts with fnmcp_ and is shown only once. A new token defaults to the three read permissions, without private:read. Your client then sends:
Authorization: Bearer fnmcp_your_token
An MCP token works on /mcp only, and it can carry a daily chip cap.
What the assistant can do
There are 20 tools, all named fair_...:
- See an overview: collections, today’s tasks, the timer and the wallet in one call.
- List and find tasks by view, project, title words or tag.
- Create, update, complete, reopen, cancel and restore tasks, and create collections.
- Start, pause, resume, extend and finish the timer, and add time spent away from the app.
- Report time by task, and read the wallet.
It cannot buy chips, invite people, or administer spaces or your account. So “plan my day from these notes, then start a 25-minute focus on the first task” works, and “top up my wallet” does not.
Create a personal API token
For scripts, use a personal API token. In Integrations, open API tokens and choose Create token:
- Name: so you recognise it later.
- Access: read-only, or read and write.
- Daily chip cap (optional): the most this token may spend in a day.
- Expiry (optional): 90 days if you leave it empty.
Copy the secret, which starts with fnpat_. It is shown only once. You can have up to 10 live API tokens.
A token reaches the REST API in every space you belong to. It can never manage credentials, sessions, devices, top-ups, gifts, webhooks, space invites or exports, or delete your account: those routes answer 403 forbidden whatever the access level. A read-only token also gets 403 on anything that changes data.
Call the REST API
The base URL is https://api.fair.ninja, and every request carries the token:
export FN_TOKEN=fnpat_your_token
curl -s https://api.fair.ninja/v1/me -H "Authorization: Bearer $FN_TOKEN"
The answer includes spaces (each with an id) and device_id, the device created for this token. You need both below.
List today’s tasks
export FN_SPACE=your_space_id
curl -s "https://api.fair.ninja/v1/spaces/$FN_SPACE/tasks?view=today&timezone=Europe/Warsaw" \
-H "Authorization: Bearer $FN_TOKEN"
view is today, upcoming, done, cancelled or all (the default). today means open tasks planned for today or earlier or with no date, plus the tasks completed today; timezone (an IANA name) decides where your day starts. done lists the newest completions first, cancelled the tasks you can still restore. The answer is {"records": [...], "next_cursor": ...}. When next_cursor is not null, pass it as cursor for the next page. A parameter the server cannot read (an unknown view, a cursor or task_id that is not an id, a limit outside 1 to 500, a from/to that is not an instant like 2026-11-04T00:00:00Z) is answered 400 validation_failed with the parameter named, and costs nothing.
Each record is readable: names next to ids, and the version numbers a later command needs in rev. A task looks like this:
{
"id": "018f2c00-1111-7000-8000-0000000000a1",
"title": "Write the homepage copy",
"status": "open",
"note": "",
"project": {"id": "018f2c00-1111-7000-8000-0000000000b1", "name": "Homepage relaunch"},
"list": {"id": "018f2c00-1111-7000-8000-0000000000b2", "name": "Inbox"},
"section": null,
"archived": false,
"planned_on": "2026-09-18",
"due_on": null,
"estimate_ms": 1500000,
"priority": 0,
"tags": [{"id": "018f2c00-1111-7000-8000-0000000000f2", "name": "writing"}],
"assignees": [],
"created_at": "2026-09-18T09:00:00.000Z",
"updated_at": "2026-09-18T09:00:00.000Z",
"completed_at": null,
"rev": {"generation": 1, "status_version": 1, "position_version": 1}
}
status is open, done or cancelled. A command that changes a task takes rev.generation as generation, and task.set_status takes rev.status_version as expected_status_version.
Also available: GET /v1/spaces/{space_id}/collections (id, name, kind, color, archived_at, rev), GET /v1/spaces/{space_id}/time_entries (with from, to, task_id; each entry names its task and project and carries focus_ms, paused_ms and rev.control_version) and GET /v1/spaces/{space_id}/time_entries/active, which answers 204 when no timer is running. Our own apps sync through a different route that carries the raw records; the readable shape is the one meant for you.
Start a timer
Changes go through one route, POST /v1/spaces/{space_id}/commands, as a command. This is the same pipeline our apps use, so a command needs a few fields a plain REST call would not. The stream field our apps send is not one of them:
op_idand the entryid: UUIDv7 values you generate. If you never saw the answer, send the exact same body again: it is not applied twice, andstatussaysduplicate. Theop_idbelongs to that body,hlcincluded, so the sameop_idwith anything changed is refused withidempotency_mismatch.hlc: a clock value,<13-digit milliseconds>-<5-digit counter>-<device id without dashes>.device_id: fromGET /v1/me.
This Python script, with no dependencies, starts a 25-minute focus session in your personal space:
import json, os, time, urllib.request, uuid
from datetime import datetime, timezone
API = "https://api.fair.ninja"
HEADERS = {
"Authorization": f"Bearer {os.environ['FN_TOKEN']}",
"Content-Type": "application/json",
}
def call(path, body=None):
data = json.dumps(body).encode() if body is not None else None
request = urllib.request.Request(API + path, data=data, headers=HEADERS)
with urllib.request.urlopen(request) as response:
return json.load(response)
def uuid7():
raw = bytearray(int(time.time() * 1000).to_bytes(6, "big") + os.urandom(10))
raw[6] = (raw[6] & 0x0F) | 0x70
raw[8] = (raw[8] & 0x3F) | 0x80
return str(uuid.UUID(bytes=bytes(raw)))
me = call("/v1/me")
space_id = next(s["id"] for s in me["spaces"] if s["kind"] == "personal")
device_id = me["device_id"]
now = datetime.now(timezone.utc)
now_ms = int(now.timestamp() * 1000)
result = call(f"/v1/spaces/{space_id}/commands", {
"op_id": uuid7(),
"space_id": space_id,
"name": "time.start",
"hlc": f"{now_ms:013d}-00000-{device_id.replace('-', '')}",
"device_id": device_id,
"payload": {
"id": uuid7(),
"kind": "focus",
"mode": "timer",
"planned_ms": 25 * 60 * 1000,
"at": now.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
},
})
print(result["status"], result.get("code"))
Add "task_id" to the payload to tie the session to a task. For a stopwatch with no plan, use "mode": "flow".
Read status in the answer, not only the HTTP code: a refused command is still HTTP 200. There are five:
applied: done.recordscarries what changed, so you do not have to read it again.noop: nothing needed changing (the same task created twice under its own id, a cancel of a task already cancelled). Not an error.duplicate: you sent thisop_idand body before. Nothing ran again;seqis the original one andrecordscarries every record the original command touched, as they are now.rejected: refused, with acode. For example,active_session_existsmeans a timer is already running in that space, and only one can run at a time;stale_generationmeans yourgenerationis old, so read the record again.conflict: someone changed the same thing first (yourexpected_*_versionis old).currentcarries the record as it is; nothing is retried for you.
A personal API token works only on https://api.fair.ninja/v1. It never works on the MCP endpoint, /mcp, which takes an OAuth sign-in or a personal MCP token (fnmcp_) instead.
Every command
All commands go to the same route, with the envelope above, and the payload shown. Ids are UUIDv7 values you generate for new records; for existing records use the ids and rev values from the reads. Text in the examples is only an example.
Lists and projects
collection.create: Create a list or a project.
{
"id": "018f2c00-1111-7000-8000-0000000000b3",
"name": "Homepage relaunch",
"kind": "project",
"color": "#e0703a"
}
collection.update: Rename or recolour a collection (only the fields you send change).
{
"id": "018f2c00-1111-7000-8000-0000000000b1",
"generation": 1,
"fields": {
"name": "Homepage v2"
}
}
collection.archive: Archive a collection (archive is not delete).
{
"id": "018f2c00-1111-7000-8000-0000000000b1",
"generation": 1,
"archived_at": "2026-09-18T09:00:00.000Z"
}
collection.delete: Delete a collection (the canonical Inbox cannot be deleted, archive it instead).
{
"id": "018f2c00-1111-7000-8000-0000000000b1",
"generation": 1
}
collection.restore: Bring a deleted collection back.
{
"id": "018f2c00-1111-7000-8000-0000000000b1",
"generation": 1
}
Sections
section.create: Add a section (a board column) to a collection.
{
"id": "018f2c00-1111-7000-8000-0000000000f1",
"collection_id": "018f2c00-1111-7000-8000-0000000000b1",
"name": "In progress",
"sort_key": "a1"
}
section.update: Rename or reorder a section.
{
"id": "018f2c00-1111-7000-8000-0000000000f1",
"generation": 1,
"fields": {
"name": "Doing"
}
}
section.delete: Delete a section.
{
"id": "018f2c00-1111-7000-8000-0000000000f1",
"generation": 1
}
section.restore: Bring a deleted section back.
{
"id": "018f2c00-1111-7000-8000-0000000000f1",
"generation": 1
}
Tasks
task.create: Create a task and file it on a list (task and placement are written together).
{
"id": "018f2c00-1111-7000-8000-0000000000a1",
"title": "Write the homepage copy",
"estimate_ms": 1500000,
"project_id": "018f2c00-1111-7000-8000-0000000000b1",
"term": {
"planned_on": "2026-09-18"
},
"placement": {
"id": "018f2c00-1111-7000-8000-0000000000c1",
"collection_id": "018f2c00-1111-7000-8000-0000000000b2",
"sort_key": "a1"
}
}
task.update: Change a task’s fields; only the keys in fields change, null clears.
{
"id": "018f2c00-1111-7000-8000-0000000000a1",
"generation": 1,
"fields": {
"title": "Write the new homepage copy",
"project_id": null
}
}
task.set_status: Complete or reopen a task (expected_status_version from the task’s rev).
{
"id": "018f2c00-1111-7000-8000-0000000000a1",
"generation": 1,
"expected_status_version": 1,
"status": "done"
}
task.delete: Cancel a task (restorable: it shows in view=cancelled).
{
"id": "018f2c00-1111-7000-8000-0000000000a1",
"generation": 1
}
task.restore: Restore a cancelled task.
{
"id": "018f2c00-1111-7000-8000-0000000000a1",
"generation": 1
}
task.set_assignee: Assign a task to a space member, or unassign.
{
"id": "018f2c00-1111-7000-8000-0000000000a1",
"user_id": "018f2c00-1111-7000-8000-0000000000f6",
"assigned": true
}
task.set_tag: Add a tag to a task, or remove it.
{
"id": "018f2c00-1111-7000-8000-0000000000a1",
"tag_id": "018f2c00-1111-7000-8000-0000000000f2",
"present": true
}
Where a task is filed
placement.move: Move a task to another list or section (expected_position_version from the task’s rev).
{
"id": "018f2c00-1111-7000-8000-0000000000c1",
"generation": 1,
"expected_position_version": 1,
"collection_id": "018f2c00-1111-7000-8000-0000000000b2",
"section_id": "018f2c00-1111-7000-8000-0000000000f1",
"sort_key": "a2"
}
placement.set_archived: Archive a task on its board, or unarchive it.
{
"id": "018f2c00-1111-7000-8000-0000000000c1",
"generation": 1,
"archived": true
}
Tags
tag.create: Create a tag (its id is derived from its name, see the protocol).
{
"id": "018f2c00-1111-7000-8000-0000000000f2",
"name": "writing",
"color": "#e05252"
}
tag.update: Rename or recolour a tag.
{
"id": "018f2c00-1111-7000-8000-0000000000f2",
"generation": 1,
"fields": {
"name": "copywriting"
}
}
tag.delete: Delete a tag.
{
"id": "018f2c00-1111-7000-8000-0000000000f2",
"generation": 1
}
tag.restore: Bring a deleted tag back.
{
"id": "018f2c00-1111-7000-8000-0000000000f2",
"generation": 1
}
Comments
comment.create: Comment on a task.
{
"id": "018f2c00-1111-7000-8000-0000000000f3",
"task_id": "018f2c00-1111-7000-8000-0000000000a1",
"body": "Looks good to me."
}
comment.update: Edit your comment.
{
"id": "018f2c00-1111-7000-8000-0000000000f3",
"generation": 1,
"body": "Looks good to me, ship it."
}
comment.delete: Delete your comment.
{
"id": "018f2c00-1111-7000-8000-0000000000f3",
"generation": 1
}
Attachments
attachment.create: Attach a link to a task.
{
"id": "018f2c00-1111-7000-8000-0000000000f4",
"task_id": "018f2c00-1111-7000-8000-0000000000a1",
"kind": "link",
"url": "https://example.com/spec.pdf",
"name": "Spec.pdf"
}
attachment.update: Rename an attachment.
{
"id": "018f2c00-1111-7000-8000-0000000000f4",
"generation": 1,
"fields": {
"name": "Spec v2.pdf"
}
}
attachment.delete: Delete an attachment.
{
"id": "018f2c00-1111-7000-8000-0000000000f4",
"generation": 1
}
Timer and time entries
time.start: Start a focus session (one timer at a time per space).
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"task_id": "018f2c00-1111-7000-8000-0000000000a1",
"kind": "focus",
"mode": "timer",
"planned_ms": 1500000,
"at": "2026-09-18T09:00:00.000Z"
}
time.pause: Pause the running session (expected_control_version from the entry’s rev).
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"expected_control_version": 1,
"at": "2026-09-18T09:10:00.000Z"
}
time.resume: Resume a paused session.
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"expected_control_version": 2,
"at": "2026-09-18T09:12:00.000Z"
}
time.finish: Finish the session; its focus time is recorded.
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"expected_control_version": 3,
"at": "2026-09-18T09:27:00.000Z"
}
time.extend: Add time to a running countdown.
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"expected_control_version": 1,
"by_ms": 300000
}
time.add_manual: Record time spent away from the app.
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"task_id": "018f2c00-1111-7000-8000-0000000000a1",
"kind": "focus",
"started_at": "2026-09-17T14:00:00.000Z",
"ended_at": "2026-09-17T14:30:00.000Z",
"note": "Call with the designer"
}
time.correct: Correct a finished entry (start, end, note or task).
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"expected_control_version": 2,
"ended_at": "2026-09-17T14:25:00.000Z",
"note": "Call with the designer (ended early)"
}
time.delete: Delete a time entry.
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"generation": 1
}
time.distraction_add: Park a thought during a session.
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"distraction": {
"id": "018f2c00-1111-7000-8000-0000000000e1",
"at": "2026-09-18T09:05:00.000Z",
"reason": "Email",
"note": "Reply to Ola",
"kind": "thought"
}
}
time.distraction_update: Edit or archive a parked thought.
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"distraction_id": "018f2c00-1111-7000-8000-0000000000e1",
"fields": {
"archived": true
}
}
time.distraction_remove: Remove a parked thought.
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"distraction_id": "018f2c00-1111-7000-8000-0000000000e1"
}
time.import: Recreate a finished entry from a backup, with its original id (idempotent on id).
{
"id": "018f2c00-1111-7000-8000-0000000000d1",
"kind": "focus",
"mode": "timer",
"manual": false,
"planned_ms": 1500000,
"started_at": "2026-09-18T09:00:00.000Z",
"ended_at": "2026-09-18T09:25:00.000Z",
"segments": [
{
"start": "2026-09-18T09:00:00.000Z",
"end": "2026-09-18T09:25:00.000Z"
}
],
"focus_ms": 1500000
}
Habits
habit.create: Create a habit with its first goal.
{
"id": "018f2c00-1111-7000-8000-0000000000f5",
"name": "Drink water",
"sort_key": "a1",
"start_on": "2026-09-24",
"timezone": "Europe/Warsaw",
"goal": {
"kind": "boolean",
"target": 1,
"direction": "at_least",
"unit": "",
"schedule": {
"type": "daily"
}
}
}
habit.update: Change a habit’s fields.
{
"id": "018f2c00-1111-7000-8000-0000000000f5",
"generation": 1,
"fields": {
"time_of_day": "morning"
}
}
habit.set_goal: Change a habit’s goal from a day on (expected_goal_version from the habit’s rev).
{
"id": "018f2c00-1111-7000-8000-0000000000f5",
"generation": 1,
"expected_goal_version": 1,
"goal": {
"effective_from": "2026-10-01",
"kind": "measurable",
"target": 8,
"direction": "at_least",
"unit": "glasses",
"schedule": {
"type": "daily"
}
}
}
habit.delete: Delete a habit.
{
"id": "018f2c00-1111-7000-8000-0000000000f5",
"generation": 1
}
habit.restore: Bring a deleted habit back.
{
"id": "018f2c00-1111-7000-8000-0000000000f5",
"generation": 1
}
Habit check-ins
checkin.set_count: Record this device’s count for a day (the check-in id is derived, see the protocol).
{
"id": "018f2c00-1111-7000-8000-0000000000f7",
"habit_id": "018f2c00-1111-7000-8000-0000000000f5",
"day": "2026-09-24",
"count": 1
}
checkin.set_override: Set a day’s value directly, replacing the counts (null clears).
{
"id": "018f2c00-1111-7000-8000-0000000000f7",
"habit_id": "018f2c00-1111-7000-8000-0000000000f5",
"day": "2026-09-24",
"expected_period_version": 1,
"value": 3
}
checkin.set_excused: Excuse a day (it does not break a streak).
{
"id": "018f2c00-1111-7000-8000-0000000000f7",
"habit_id": "018f2c00-1111-7000-8000-0000000000f5",
"day": "2026-09-24",
"excused": true
}
checkin.set_note: Write a note on a day.
{
"id": "018f2c00-1111-7000-8000-0000000000f7",
"habit_id": "018f2c00-1111-7000-8000-0000000000f5",
"day": "2026-09-24",
"note": "Felt great"
}
App settings
app_settings.set: Change the person’s own app settings (personal space only; the id is derived, see the protocol).
{
"id": "018f2c00-1111-7000-8000-0000000000f8",
"fields": {
"session_length_ms": 1500000
}
}
Set up webhooks
A webhook is a call from us to your server when something changes: a task is completed, a timer finishes. Webhooks belong to a space, and each space can have up to 20 endpoints.
Create an endpoint
- In Integrations, open Webhooks and choose Add webhook.
- Enter an
httpsURL. It must be publicly reachable: private addresses and redirects are refused. - Choose all events or a selection. The full list is public at
/v1/capabilities, underfeatures.event_types. - Choose the payload: Readable (the default: the record with names and titles, a one-sentence
summaryand the fields thatchanges), Metadata only (ids and versions, no titles or notes) or Raw (our internal sync records, for tools that mirror them). - Save, and copy the signing secret. It starts with
whsec_and is shown only once.
Verify the signature
Check every request before you trust it. Signatures follow the Standard Webhooks specification, so its libraries work too. Each request carries three headers:
webhook-id: the delivery id, the same on every retry.webhook-timestamp: Unix seconds.webhook-signature:v1,<base64 signature>. During a secret rotation there are two, separated by a space.
The signature is an HMAC-SHA256 of {id}.{timestamp}.{raw body}. The key is the base64-decoded part of the secret after whsec_. Use the raw bytes of the body, before any JSON parsing:
import base64, hashlib, hmac, time
def is_valid(secret, headers, body, tolerance_seconds=300):
delivery_id = headers["webhook-id"]
timestamp = headers["webhook-timestamp"]
if abs(time.time() - int(timestamp)) > tolerance_seconds:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{delivery_id}.{timestamp}.".encode() + body
digest = hmac.new(key, signed, hashlib.sha256).digest()
expected = "v1," + base64.b64encode(digest).decode()
return any(hmac.compare_digest(expected, s) for s in headers["webhook-signature"].split(" "))
A readable delivery of a new task looks like this (shortened):
{
"id": "018f2c00-2222-7000-9000-000000000001",
"type": "task.created",
"space_id": "018f2c00-1111-7000-8000-000000000001",
"occurred_at": "2026-09-18T09:00:01.000Z",
"seq": 1,
"actor": {"user_id": "018f2c00-1111-7000-8000-000000000002", "display_name": "Tomek"},
"summary": "Tomek created task “Write the homepage copy” in project “Homepage relaunch”.",
"data": {"task": {"id": "018f2c00-1111-7000-8000-0000000000a1", "title": "Write the homepage copy", "list": {"id": "018f2c00-1111-7000-8000-0000000000b2", "name": "Inbox"}, "rev": {"generation": 1, "status_version": 1}}},
"payload_mode": "readable",
"api_version": 1
}
An update also carries changes, for example {"title": {"from": "Old title", "to": "New title"}}. Every event type has a full sample in the portal’s event list.
Answer with any 2xx within 10 seconds. Delivery is at-least-once and unordered, so ignore a webhook-id you have already handled, and use seq in the body when order matters.
Test it, and read the deliveries log
Send test fires a real webhook.ping event through the real pipeline, except on a paused or disabled endpoint, which refuses it until you resume it. Deliveries shows each attempt with its outcome. Skipped attempts appear there too, with the reason, so a quiet webhook can be explained.
Rotate the secret
Choose Rotate secret in the endpoint’s menu. You get a new secret, shown once, and for the next 24 hours both secrets sign every request. Your code keeps working while you switch.
What happens when your server fails
- Retries: a delivery gets up to 8 attempts, with growing waits from 5 seconds to 6 hours, and never beyond 72 hours after the event. The eighth attempt comes about 9 to 10 hours after the first.
- Which answers count as failure: everything except
2xxand410, including other4xx, is retried. 410 Gone: disables the endpoint. The portal shows why. Use this to switch a webhook off from your side.- Circuit breaker: after 50 failed attempts in a row, across all deliveries to that endpoint, it pauses for 72 hours, then resumes by itself. Resume now in its menu lifts the pause sooner. Events during a pause are not queued for later.
What it costs
There is no plan to buy. fair.ninja runs on prepaid chips, 100 chips for 1 USD.
| What | Price |
|---|---|
| One list page read or the active timer read through an API token | 0.001 chip |
| One read tool call through MCP | 0.001 chip |
| One accepted change (a command), from anywhere | 0.001 chip |
| One webhook delivery, retries included | 0.002 chip |
Reads by your own apps are never priced, and neither is GET /v1/me. A refused or failed read costs nothing. An empty page is still a read.
The account fee covers ordinary use. The fee is 0.5 chip a day, taken at the start of each day. That day’s usage is covered up to the same amount, and you pay only for what goes above it. So a day costs the larger of the fee and your usage: 500 priced calls a day cost nothing extra, if nothing else was used that day.
On an Empty balance, everything priced stops. API and MCP calls are refused with wallet_exhausted, reads and the timer included, and webhook deliveries are skipped. The only thing an integration can still read is your account’s own state, such as the balance. Add chips in the wallet and it all resumes. After 30 days without a top-up, the server copy is deleted.
A token over its daily cap gets 429 api_token_cap_exceeded. Too many requests get 429 rate_limited with a Retry-After header. In a space someone else owns, this includes a limit on commands per minute, because the owner pays for them.
Where to go next
- Integrations in the account portal
- Service details, for the full price list
- Security