FakeParrot API
Updated October 1, 2026
The FakeParrot API is the REST API behind the FakeParrot web app, the Mac app,
the fakeparrot CLI and the MCP server. Your code calls the same routes the
web app does. Every request acts as the person who created the token, so it
sees and changes only what that person can.
Base URL and reference
- Base URL:
https://fakeparrot.com. API routes start with/api/, for examplehttps://fakeparrot.com/api/wiki/search. - OpenAPI document:
https://fakeparrot.com/openapi.json. It lists registered routes and request schemas. Some success schemas are empty or generic, so it does not yet describe every response. It also includes internal and operator routes; inclusion does not grant token admission. Fetch it without signing in. For a small contract, use the discovery flow below. - Requests and responses are JSON unless a route says otherwise, for example a file upload or download.
Not built yet:
- Version numbers. Paths have no version, such as
/v1. The OpenAPI document always describes the routes that are live now. - An interactive reference viewer. Machine-readable operation contracts are hosted below; open them in an OpenAPI viewer if needed.
- Client libraries. There is no official SDK. Use any HTTP client.
Find one operation
fakeparrot describe "custom-schedule"
describe searches GET /api/developer/operations?q=custom-schedule using the
caller's existing sign-in. It returns operation IDs, methods, paths, required
scopes, known live prerequisites, token_admitted, unavailable_reason,
missing_scopes, next_action and a contract_url. A single match also includes
its complete contract. Native CLI updates ship separately; if your installed
reply lacks token_admitted, use fakeparrot call GET /api/developer/operations --query q=custom-schedule < /dev/null. This uses the same existing sign-in.
For several matches, fetch just the chosen contract:
fakeparrot call GET /api/developer/contracts/OPERATION_ID
MCP discover_operations takes a query and returns the same discovery facts.
Without sign-in, GET /api/developer/contracts?q=custom-schedule supplies the
public index. Each operation contract contains its request, declared responses,
errors, authentication and all referenced schemas. The full OpenAPI file remains
available as an optional bundle.
token_admitted describes route admission and known live prerequisites. A route
can still refuse a particular page, workflow or other target. Discovery reveals
no resource names. route_specific authentication means a route uses its own
credentials or checks; inspect its contract before calling it. Operator requirements
still need live grants, and a scope does not confer an operator grant.
Retain schema_revision; reload the chosen contract when it changes. Accept
additional response fields. Some nested extension sections and uncovered operations
still have generic schemas; do not infer guarantees beyond their declared fields.
Deprecated metadata and explicit 410 responses describe retired operations.
Paths do not promise a versioned compatibility window.
Sign in
Send a token on every request:
Authorization: Bearer fkp_...
There are two ways to get one. Both give an fkp_ token that works the same
way on the API and on the MCP server.
| Way | Best for | How long it works |
|---|---|---|
| API key | Your own scripts and servers | Until you revoke it |
| OAuth 2.1 with PKCE | An app that other people sign in to | Until 30 days pass with no use, or it is revoked |
API key
- In FakeParrot, open Connections → Agents → Signed in.
- Choose Create API key, name it, pick its permissions, and copy it. FakeParrot shows the key only once.
- Send it as
Authorization: Bearer fkp_...
Each person can hold up to 25 live tokens, counting both keys and OAuth sign-ins. Create one key per app, so you can revoke each on its own. Revoke a key on the same tab. A key cannot create or revoke keys; only a person signed in to the web app can.
OAuth
FakeParrot runs a standard OAuth 2.1 server with PKCE and dynamic client registration. The MCP server guide has the steps under Sign in with OAuth. The same tokens work on the API.
What to know about OAuth tokens:
- Clients are public: registration returns a client ID (
fpc_...) and no client secret. - The token response includes
expires_in, which is 30 days in seconds. The 30 days restart each time the token is used. - Ask for
offline_accessto get a refresh token (fpr_...). It has no expiry date of its own. Each refresh returns a new refresh token; using an old one again revokes every token from that sign-in. - An access token also stops working when the person revokes it or leaves the company.
- A refresh token stops working when the person revokes the sign-in, signs out, leaves the company, or authorizes the same client again.
- Errors from
POST /api/oauth/tokenuse the OAuth format,{"error": "invalid_grant", "error_description": "..."}, not the format under Errors below.
Scopes
A token holds one or more of these six scopes. OAuth gives all six when the
client leaves out scope.
| Scope | What it allows |
|---|---|
wiki:read | Search, list and read wiki pages and files, compare versions, and read comments. |
wiki:write | Create, edit, move and delete pages and folders, and post comments. Includes wiki:read. |
workflows:read | List workflows and read their setup, history and run reports. |
workflows:run | Start a saved workflow. Includes workflows:read. |
workflows:write | Create, change, schedule, turn on and turn off workflows. Includes workflows:read. |
ops:read | Read workflow and connection health, the team directory, and reports from connected systems. |
workflows:write does not include workflows:run. Building a workflow and
spending money running it are separate permissions.
A token with all six scopes reaches every /api route its person can use
in the web app. The route's own checks decide the rest, such as which pages the
person may read. A few routes always refuse a token, and the 403 says why:
- creating or revoking keys, sign-ins and sessions;
- changing who can see something, such as share links, access lists and memberships;
- billing changes, and reading billing without the
billing:readscope; - accepting legal terms;
- reading or restoring deleted items in the trash.
A token with fewer scopes reaches only the route groups its scopes cover,
such as /api/wiki for wiki:read. It can write only where one of its scopes
allows that write.
billing:read is an extra scope that reads the company's billing status. It
is not one of the six, so a token has it only when someone picks it.
Example request
curl "https://fakeparrot.com/api/wiki/search?q=expense%20policy" \
-H "Authorization: Bearer $FAKEPARROT_API_KEY"
This needs wiki:read. The reply lists the pages the person can read that
match the search.
Errors
Most API errors use this JSON shape. OAuth errors use the separate format above:
{
"success": false,
"error": {
"code": "ACCESS_DENIED",
"message": "API token is missing the wiki:write scope",
"timestamp": "2026-10-01T09:30:00.000000Z",
"request_id": "…",
"details": { "error": "insufficient_scope", "scope": "wiki:write" }
}
}
codeis a fixed, machine-readable name. Branch on it, not onmessage.messageis written for a person.request_idmatches theX-Request-Idheader on every response. Include it when you contact support.detailsappears only when there is more to say.
Common codes:
| Status | code | Meaning |
|---|---|---|
400 | VALIDATION_ERROR | The request did not match the route's schema. details.validation_errors lists each problem and details.field names the first. Routes under /api/execution and /api/local-file-catalogs send this with 422. |
400 | BAD_REQUEST | The route refused the request. message says why. |
401 | AUTH_REQUIRED | The token is missing, invalid or revoked. |
403 | ACCESS_DENIED | The person may not do this, or the token cannot reach this route. A missing scope adds details.error set to insufficient_scope, details.scope, and a matching WWW-Authenticate header. |
404 | NOT_FOUND | The route or item was not found. |
429 | RATE_LIMITED | See Rate limits. |
500 | INTERNAL_ERROR | Something failed on our side. A write may already have happened; reconcile it before retrying. Include request_id when contacting support. |
503 | DB_POOL_EXHAUSTED | The server is briefly at capacity. Honor Retry-After. For a write, also follow its retry contract. |
Some routes return their own codes for their own cases. The OpenAPI document
and the message describe them.
Rate limits
There is no blanket request throttle on /api, /mcp or OAuth. Individual
operations can still have execution limits or capacity responses:
- Public share links under
/public/allow 3,000 requests a minute from one IP address, then answer429. - Sync routes under
/api/wiki/sync/v2/may answer429withRetry-Afterwhen a server is busy, including calls from CLI or API clients. - On-demand workflow starts have a configurable per-workflow hourly limit.
The implementation default is 60; this is not a measurement of the deployed
setting. A refused start can answer
429. - Database capacity can answer
503withRetry-After.
A workflow that calls a connected system, such as HubSpot or Google, is still subject to that system's own limits.
Agent recipes
Examples use representative IDs and response excerpts. Substitute IDs from a readable result; do not invent them or infer numeric IDs from UUIDs. Authentication is set up once. CLI examples use its existing sign-in; REST uses your existing token. An access denial is not a reason to retry under a different identity.
Read and edit one page
Needs wiki:read to search/read, wiki:write to edit, and current write access
to the page. On a connected Mac, read a known page from the folder. Search an
unknown page through the server:
fakeparrot wiki search "expense policy"
fakeparrot wiki get PAGE_UUID
For REST, use GET /api/wiki/search?q=expense%20policy, then
GET /api/wiki/pages/PAGE_UUID. A successful HTTP read can return access: null
and only page metadata. Proceed only when the response includes readable content.
Use the returned page.id (integer), page.public_id (UUID), page.current_version
and page.content_md; preserve the title and every passage outside the requested change.
MCP search_wiki returns results with numeric page_id, page_public_id,
path, title, version and url. MCP read_page takes the numeric
page_id or a supported slug path and returns page metadata plus markdown.
Use numeric IDs for the MCP tools ending in _by_id; their schemas do not accept
public UUIDs. REST page routes accept the public UUID or legacy numeric ID.
For an exact text change, prefer the existing edit operation. Mint one UUID v4, save it with the request before sending, and reuse both only for an exact retry:
printf '%s' '{"expected_version":7,"mutation_id":"cf39993a-eccd-4489-9d28-7bdcaa356f30","edits":[{"kind":"replace","find":"30 days","replace":"45 days","all":false}]}' \
| fakeparrot call POST /api/wiki/pages/PAGE_UUID/edits
MCP uses edit_page_by_id with the same edit fields and a numeric page_id.
The response has outcome, page, base_version, current_version, edits,
mutation, refusal, overtaken, attempts, max_attempts and note:
outcome | What to do |
|---|---|
applied | One version was written. Read the page again and confirm the intended text and current version. |
no_change | No version was written. Inspect edit counts and read the page; an absent match does not prove the requested text is already present. |
conflict | Another save kept moving the page. Read the current page, re-evaluate the edit and mint a new mutation ID for a revised request. |
refused | Inspect refusal; an anchor can be absent or ambiguous, or edits can overlap. Correct the request with a new mutation ID. |
overtaken | Another save removed the target text. Read the current page and decide whether a new edit is still needed. |
Server retries can re-plan an untouched passage over a newer version. Report any
note about a concurrent save. Client retries after a lost response keep the original
request and mutation ID; changing the body or observed version requires a new ID.
A reused ID with different content is refused.
For a complete replacement, use PUT /api/wiki/pages/PAGE_UUID/content with
title, markdown, expected_version and mutation_id, or MCP
save_page_by_id. Append through POST /api/wiki/pages/PAGE_UUID/appends or
append_to_page_by_id with markdown, expected_version and mutation_id.
Inspect the mutation outcome: newer_version_available can preserve your content
as saved_draft_version without replacing the current head; stale_noop does
not write. Read back the current head before reporting completion. Legacy
save_page and append_to_page do not provide the same caller mutation identity.
Start and follow one workflow
Needs workflows:read to discover/read, workflows:run to start, current run
permission and access to the resulting report. workflows:write alone does not
allow a run. Some owner-bound workflows execute with their owner's source authority;
the initiating caller and execution actor can differ. Discovery does not grant
run permission or report visibility.
fakeparrot workflow list
fakeparrot workflow get WORKFLOW_UUID
printf '%s' '{"inputs":{},"run_request_id":"70de57be-8548-40cd-b67d-a737b95224bd"}' \
| fakeparrot call POST /api/workflows/WORKFLOW_UUID/run
Use the workflow's public UUID from the list; load its declared input schema and
supply the required values. REST can test-run a draft and refuses archived/deleted
workflows. MCP run_workflow accepts draft or active workflows,
workflow (returned ID, display number or exact name), inputs, run_request_id
and wait_seconds.
Use an ID to avoid name ambiguity.
A REST start answers 201, for example:
{"success":true,"run_id":"96400b64-fb90-4f42-8ca6-b3cd0f2de812","status":"running"}
This acknowledges a start. Persist run_id before doing anything else; it is also
the report's public ID. Follow it with GET /api/reports/RUN_UUID or:
fakeparrot report get RUN_UUID
fakeparrot report events RUN_UUID
The REST report has schema_version: 2. Read execution status at
presentation.execution_status, and inspect presentation.outcome, states,
deliverables and delivery_receipts within presentation. The CLI wraps this
body in its command envelope. In a combined workflow run, the report is at
body.report.body; its status is body.report.body.presentation.execution_status.
MCP get_workflow_run takes run_id and returns top-level status, outcome,
errors, stages and action_execution when present. These projections have
different shapes; use their declared schemas rather than assuming identical bodies.
Check execution status and the stored outcome and required deliverables:
| Status | Meaning |
|---|---|
queued, running | Work is pending. Continue reading the same run. |
awaiting_work_packet | Waiting for delegated work to return. The report is not complete. |
awaiting_approval | Historical approval pause. Retained as evidence; no approval action is available. |
completed | Execution ended. Verify the actual outcome and required deliverables before reporting success. |
failed, cancelled, stale, budget_exceeded | Execution ended without successful completion. Read errors/events and report the result. |
fakeparrot workflow run combines start, polling and events. Its ok and exit
code describe command completion, even for a failed or paused run. A wait timeout
ends observation and does not cancel work. Resume reading the same report.
For a retry-safe start, mint a UUID v4 run_request_id before sending and persist
it with the exact request. REST, CLI --run-request-id and MCP accept it. An exact
retry by the same person in the same company recovers the original run, including
after credential rotation. The key binds the workflow, inputs and title; reuse with
different intent returns 409 WORKFLOW_RUN_REQUEST_CONFLICT. Omitted inputs and
an empty input object are equivalent. MCP's transport attribution is excluded from
the identity, so the same input request can be recovered through REST or MCP.
Current run authority is checked again; report access is checked separately.
Archiving stops new starts but allows recovery of an existing receipt when the
caller retains authority; a deleted or unreadable target remains unavailable.
Concurrent exact retries reserve one run and increment its call count once. The receipt commits with the initialized run before dispatch; the existing dispatcher can recover committed work. Receipts stay with company/run history. After a run is permanently removed, the key is retained and recovery returns unavailable instead of starting another run. This deduplicates workflow starts, not external side effects. To intentionally start new work, mint a new key.
A start without a caller retry identity can create another run if repeated. After
an uncertain response, inspect the workflow's run list and match caller, start time
and inputs; if there is no unique match, report the uncertainty before starting
another paid action. Read calls can be retried; writes need their own retry contract.
Do not treat 500, a disconnect or a missing response as proof that nothing happened.
Receive, verify and recover report callbacks
Callbacks send the same JSON shape as GET /api/reports/{report_id}, built
with the hook owner's current access. All routes below cost workflows:read
for bearer tokens. Registration requires a bearer credential; a browser session
can read, rotate, replay or end its own hooks. Ownership stays with the person
and original credential: an OAuth refresh family survives access-token rotation;
a plain key's revocation ends delivery. A new key does not revive a revoked hook.
| Operation | Request and result |
|---|---|
POST /api/reports/{report_id}/callbacks | {"url":"https://receiver.example/callback"} → 201, one hook for this report; sends at once if already finished. |
POST /api/report-hooks | target_url, optional workflow_public_id and statuses → 201, a subscription. |
GET /api/report-hooks | Your live hooks, with masked addresses and signing key IDs. |
POST /api/report-hooks/{hook_id}/signing-key | No body → 200, replace the signing key or enable signing on a legacy hook. |
GET /api/report-hooks/{hook_id}/deliveries | Optional limit (default 50, maximum 100) and offset → delivery window and next_offset. Continue until null; current report access can hide rows in a window. |
GET /api/report-hooks/{hook_id}/deliveries/{delivery_id} | Current delivery state, attempt totals, timestamps and a sanitized failure code. |
POST /api/report-hooks/{hook_id}/deliveries/{delivery_id}/replay | {"replay_count":0} → 200, queues the eligible failed delivery once for that observed generation. |
DELETE /api/report-hooks/{hook_id} | 204, stops delivery and removes stored signing material. Repeating it succeeds. |
Hook and delivery IDs are UUIDs. Delivery history includes ended hooks when you
retain their IDs. Only the owner in the same company can read it; every report
also needs current read access. Another person's hook or unreadable report
returns 404, including for admins. History lasts while the hook, owner and run
remain stored; ending a hook does not delete its history.
Registration adds signing_key_id and a one-time signing_secret to the existing
hook response. Rotation returns hook_id, signing_key_id and signing_secret.
These responses use Cache-Control: no-store. Save the secret in your receiver's
secret store; never put it in the callback URL, a log, or source control. It is
32 random bytes represented as 64 hex characters. Use those characters as the
UTF-8 HMAC key; do not hex-decode them. FakeParrot stores it encrypted and never
returns it through listings, history, discovery or later reads. If a registration
response is lost, find the hook in your list. If a key response is lost, rotate
again; rotation itself is not idempotent.
Existing hooks remain unsigned until you call signing-key. New hooks are
signed by default, retaining the existing JSON and delivery headers. Receivers
that ignore additional headers remain compatible. An already-finished report
can arrive before its registration response: return a retryable response such
as 503 until the receiver has saved its verification key.
Verify before parsing or processing
Each signed POST includes:
X-FakeParrot-Delivery: the durable delivery UUID.X-FakeParrot-Hook: the hook UUID.X-FakeParrot-Key-Id: the signing key UUID.X-FakeParrot-Timestamp: Unix seconds for this attempt.X-FakeParrot-Signature:v1=followed by lowercase HMAC-SHA256 hex.
The signed bytes are the UTF-8 prefix
v1.TIMESTAMP.DELIVERY_UUID.HOOK_UUID.KEY_UUID. followed by the raw HTTP body
bytes. Do not parse and re-serialize JSON before verification. Match the hook
and key ID against your receiver configuration, use a constant-time comparison,
and check timestamp freshness under your receiver's policy. For example:
import hashlib
import hmac
import time
# secret and known IDs come from your receiver's protected configuration.
def verify(headers, raw_body, hook_id, key_id, secret):
if headers.get("X-FakeParrot-Hook") != hook_id:
return False
if headers.get("X-FakeParrot-Key-Id") != key_id:
return False
timestamp = headers.get("X-FakeParrot-Timestamp", "")
try:
if abs(time.time() - int(timestamp)) > 300:
return False
except ValueError:
return False
delivery_id = headers.get("X-FakeParrot-Delivery", "")
prefix = f"v1.{timestamp}.{delivery_id}.{hook_id}.{key_id}.".encode()
expected = "v1=" + hmac.new(
secret.encode(), prefix + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(
expected, headers.get("X-FakeParrot-Signature", "")
)
Rotation replaces the server's signing material immediately. Future attempts
read the current key, including retries and replay. An already-in-flight request
may carry the previous key; keep both receiver keys briefly when handling a
planned rotation, then remove the old one. If compromised, remove the old key
immediately and reject those requests. Ending the hook prevents new sends;
a POST already in flight may still arrive. Missing or unreadable signing material
fails closed with signing_key_unavailable; it never downgrades to unsigned.
Delivery outcomes and replay
A delivery returns id, hook_id, report_id, state, attempts,
replay_count, created_at, last_attempt_at, next_attempt_at, delivered_at
and failure_reason. States are pending, delivered, failed or cancelled.
delivered means the receiver answered 2xx; it does not prove its downstream
action succeeded. Failure details are codes such as http_400, timeout,
blocked or request_error, never receiver response bodies or addresses.
attempts counts settled network attempts over the delivery's lifetime; a worker
crash before settlement can leave an uncounted request.
Transport failures, 408, 425, 429 and server errors retry automatically:
up to eight attempts per initial delivery or replay, with 30-second backoff
doubling to at most one hour. Redirects are never followed. Other HTTP responses
fail the delivery; 410 also ends the hook. Signing failures are recorded without
a network attempt. A subscription survives an ordinary delivery failure; a
one-report callback ends as undeliverable and can be reopened by replay.
Read a failed delivery, repair the receiver or signing key, then POST its current
replay_count. The original row and ID remain; replay_count increases by one,
the retry budget restarts, and attempt totals remain. Repeating the same observed
count recovers that replay's current outcome, even if it has failed again. To
request a further replay, read and submit the new count. A stale count or an
ineligible state returns 409. Delivered and cancelled deliveries cannot start
another replay. An unsubscribed hook, a 410 receiver, or an ended credential
cannot be revived. Replay checks the caller's access, the original credential's
live scopes, company membership and report access, and delivery checks them again
before sending. Refresh in progress returns 409; lost credential authority
returns 403. A successful replay response means recovery was accepted; read
its delivery state to learn whether the receiver acknowledged it.
Delivery is at least once. X-FakeParrot-Delivery stays the same across automatic
retries, lease-expiry recovery and replay. X-FakeParrot-Hook and the report ID also
stay the same. The body is rebuilt from current readable report state on every
attempt, so its bytes can change under the same delivery ID. Timestamps and
signatures are recomputed, and the signing key ID changes on rotation.
After verifying, atomically record the delivery ID with your receiver's effect or durable work item. An authenticated duplicate must receive a successful acknowledgement without repeating the effect, even if its report bytes differ. Separate registrations get separate hook and delivery IDs. Keep deduplication records for as long as you allow replay. FakeParrot does not promise exactly-once external effects; downstream systems need their own idempotency support.
The CLI reaches all these routes with fakeparrot call. MCP exposes
list_report_hook_deliveries, get_report_hook_delivery,
replay_report_hook_delivery and rotate_report_hook_signing_key with the same
services, scopes and live authorization. For registration and status filters,
see the Zapier and Make guide.
Author a workflow and confirm it was saved
Needs workflows:write and the caller's live authoring permissions. Load
GET /api/workflow-capabilities or MCP list_workflow_capabilities, then reuse an
existing workflow by revising it when the task concerns that workflow.
fakeparrot describe /api/workflows/blocks/design
printf '%s' '{"job":"Every Monday send me a pipeline summary","start_as":"active","wait":false,"authoring_request_id":"4c88a0ef-cd93-4b71-87a1-3af9e35e2eec"}' \
| fakeparrot call POST /api/workflows/blocks/design
The REST create route defaults to start_as: "draft"; choose active when the
requested task includes scheduling. Draft is a caller choice, not an extra approval
step. MCP create_workflow defaults to active and requires the intended visibility
and delivery destination. Supply the user's actual job; the service composes the
workflow. The REST route takes the same choices as MCP: visibility (company by
default, team with team, or private), output.create_page for a wiki page,
trigger.schedule, model, source_requirements, report_spec, intent and
execution_mode. Nothing is inferred from the job's words: without
output.create_page the workflow writes no page. Inspect the current schema for
each field's shape.
A background response returns operation_id and status_url with
status: "composing". Poll the returned status URL or MCP
get_workflow_authoring_operation. saved means the definition was saved; it does
not prove a requested run completed. Read the resulting workflow and schedule,
then follow any returned run ID separately. Relay clarification, capability-gap
and failure outcomes instead of claiming a workflow was created.
An exact retry with the same authoring_request_id reuses the existing operation
for that caller and company. Different intent under the same ID conflicts. Preserve
the request and receipt across reconnects; revised intent needs a new ID.
Help
Email support@fakeparrot.com with the
request_id from the error. Report security issues to
security@fakeparrot.com.