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

Not built yet:

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.

WayBest forHow long it works
API keyYour own scripts and serversUntil you revoke it
OAuth 2.1 with PKCEAn app that other people sign in toUntil 30 days pass with no use, or it is revoked

API key

  1. In FakeParrot, open Connections → Agents → Signed in.
  2. Choose Create API key, name it, pick its permissions, and copy it. FakeParrot shows the key only once.
  3. 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:

Scopes

A token holds one or more of these six scopes. OAuth gives all six when the client leaves out scope.

ScopeWhat it allows
wiki:readSearch, list and read wiki pages and files, compare versions, and read comments.
wiki:writeCreate, edit, move and delete pages and folders, and post comments. Includes wiki:read.
workflows:readList workflows and read their setup, history and run reports.
workflows:runStart a saved workflow. Includes workflows:read.
workflows:writeCreate, change, schedule, turn on and turn off workflows. Includes workflows:read.
ops:readRead 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:

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" }
  }
}

Common codes:

StatuscodeMeaning
400VALIDATION_ERRORThe 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.
400BAD_REQUESTThe route refused the request. message says why.
401AUTH_REQUIREDThe token is missing, invalid or revoked.
403ACCESS_DENIEDThe 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.
404NOT_FOUNDThe route or item was not found.
429RATE_LIMITEDSee Rate limits.
500INTERNAL_ERRORSomething failed on our side. A write may already have happened; reconcile it before retrying. Include request_id when contacting support.
503DB_POOL_EXHAUSTEDThe 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:

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:

outcomeWhat to do
appliedOne version was written. Read the page again and confirm the intended text and current version.
no_changeNo version was written. Inspect edit counts and read the page; an absent match does not prove the requested text is already present.
conflictAnother save kept moving the page. Read the current page, re-evaluate the edit and mint a new mutation ID for a revised request.
refusedInspect refusal; an anchor can be absent or ambiguous, or edits can overlap. Correct the request with a new mutation ID.
overtakenAnother 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:

StatusMeaning
queued, runningWork is pending. Continue reading the same run.
awaiting_work_packetWaiting for delegated work to return. The report is not complete.
awaiting_approvalHistorical approval pause. Retained as evidence; no approval action is available.
completedExecution ended. Verify the actual outcome and required deliverables before reporting success.
failed, cancelled, stale, budget_exceededExecution 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.

OperationRequest 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-hookstarget_url, optional workflow_public_id and statuses → 201, a subscription.
GET /api/report-hooksYour live hooks, with masked addresses and signing key IDs.
POST /api/report-hooks/{hook_id}/signing-keyNo body → 200, replace the signing key or enable signing on a legacy hook.
GET /api/report-hooks/{hook_id}/deliveriesOptional 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:

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.