Developers

A REST API, a remote MCP server and signed webhooks. One key opens all three.

Everything the panel does, your own software can do. You create an API key in the panel, exchange it for a short-lived token, and call the same endpoints the panel calls. The remote MCP server puts the same capabilities in front of an AI client such as Claude or ChatGPT, and webhooks push events to you instead of you polling for them.

Quick start

1. Create a key. Sign in to the panel and open Settings → Integrations → API keys. Pick full management or read-only, give the key a label, and copy the value. It is shown once and never again — we store only a hash of it.

2. Exchange it for a token. The long-lived key never travels on normal requests. You trade it for a JSON Web Token that expires within the hour, so a stolen token dies quickly.

curl -X POST https://api.officialaiagent.com/v1/auth/token \
  -H "Authorization: Bearer <YOUR_KEY>"   # -> {"access_token": "<JWT>"}

curl https://api.officialaiagent.com/v1/calls \
  -H "Authorization: Bearer <JWT>"

3. Call the endpoints. Every later request carries Authorization: Bearer <JWT>. When the token expires you get a 401: refresh once and retry. A second 401 is a real permission error and should be surfaced to a human, not retried in a loop.

The base URL for this environment is https://api.officialaiagent.com. Every example on this page is generated from it, so nothing here points at a host that does not exist.

MCP setup

The remote MCP server lives at https://api.officialaiagent.com/mcp and speaks the streamable HTTP transport. Authentication is the same API key, sent as a bearer token. Below is the configuration for four common clients.

Claude Desktop

claude_desktop_config.json

{
  "mcpServers": {
    "fideliscall": {
      "type": "http",
      "url": "https://api.officialaiagent.com/mcp",
      "headers": {
        "Authorization": "Bearer <YOUR_KEY>"
      }
    }
  }
}

Claude Code

Terminal

claude mcp add --transport http fideliscall \
  https://api.officialaiagent.com/mcp \
  --header "Authorization: Bearer <YOUR_KEY>"

ChatGPT

Settings → Connectors → Add custom connector

MCP server URL: https://api.officialaiagent.com/mcp
Authorization: Bearer <YOUR_KEY>

Cursor

~/.cursor/mcp.json

{
  "mcpServers": {
    "fideliscall": {
      "type": "http",
      "url": "https://api.officialaiagent.com/mcp",
      "headers": {
        "Authorization": "Bearer <YOUR_KEY>"
      }
    }
  }
}

Replace <YOUR_KEY> with the key you created. A read-only key can call the read tools and will get a 403 on the write ones — that is the intended behaviour, not a bug.

Tools

Each tool needs one scope. If your key does not carry it, the call is refused before anything is written.

MCP tools and the scope each one requires
ToolScopeWhat it is
get_overviewconfig:readRead only
get_manager_briefcalls:readRead only
analyze_callcalls:writeWrites to your business
list_servicesconfig:readRead only
list_modelsconfig:readRead only
list_closuresconfig:readRead only
list_recent_callscalls:readRead only
update_settingconfig:writeWrites to your business
add_closureconfig:writeWrites to your business
remove_closureconfig:writeWrites to your business
add_serviceconfig:writeWrites to your business
update_serviceconfig:writeWrites to your business
save_knowledgekb:write (also accepts config:write during the migration window)Writes to your business
list_bookingscalls:readRead only
list_open_slotscalls:readRead only
move_bookingcalls:writeWrites to your business
request_capabilityconfig:writeWrites to your business
list_capability_requestsconfig:readRead only
get_capability_requestconfig:readRead only
submit_capability_feedbackconfig:writeWrites to your business
list_team_daycalls:readRead only
list_tasksconfig:readRead only
complete_taskconfig:writeWrites to your business
get_attentioncalls:readRead only
get_usagecalls:readRead only
get_setup_gapsconfig:readRead only
list_action_plansconfig:readRead only
get_revenuecalls:readRead only
get_occupancycalls:readRead only
get_call_qualitycalls:readRead only
get_human_requestscalls:readRead only
get_customer_rhythmcalls:readRead only
list_staff_agendacalls:readRead only
list_findingscalls:readRead only
list_webhooksconfig:readRead only
dismiss_findingcalls:writeWrites to your business
draft_action_planconfig:writeWrites to your business
add_staffconfig:writeWrites to your business
update_staffconfig:writeWrites to your business
assign_taskconfig:writeWrites to your business
create_webhookconfig:writeWrites to your business

Scopes

A key carries a set of scopes. "Full management" asks the server to pick the whole self-service set, so a key created today keeps working when we add a capability. "Read only" is the narrow set marked below.

Scopes available to a key created from the panel
ScopeRead-only keyOpens
config:readyesAssistant settings, services, closures, knowledge map
config:writenoWrite settings, services and closures
calls:readyesCalls and chats: list, detail, transcript, callers
calls:writenoBookings and action items
kb:readyesKnowledge base documents: list, content, preview
kb:writenoWrite to and delete from the knowledge base
s2s:mintnever issuedThe voice worker's own short-lived identity

s2s:mint is listed for completeness and is never issued from the panel. It is the voice worker's own identity and is minted server-side with a lifetime measured in seconds.

Rate limits

Limited endpoints carry two ceilings. The narrow one is per key or user; if that passes, the wider per-business one applies. The order is deliberate: a key over its own ceiling should not eat the shared budget on its way to being refused.

Per-minute rate limit lanes
LanePer businessPer key or user
Reads (list, detail)300 / min120 / min
Writes (POST, PATCH, DELETE)120 / min60 / min
Analytics (/v1/analytics*)60 / min30 / min

Endpoints that spend money or write to a ledger have their own hourly bucket on top of the per-minute lane:

Hourly buckets on spending endpoints
EndpointPer businessPer key or user
POST /v1/kb/ingest/commit/*30 / hour
POST /v1/manager/diagnostics/calls/{id}60 / hour30 / hour
POST /v1/manager/action-plans/{id}/execute30 / hour15 / hour
POST /v1/api-keys10 / hour
Not every endpoint is limited today. Roughly half of the /v1 surface has no server-side ceiling — /v1/config included. Write your client as if it were limited anyway: implement your own back-off rather than trusting us to stop you.

A rate-limit refusal looks like this:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

{"error": {"code": "rate_limited", "reason": "principal_window"}}

Branch on reason, never on the human-readable message. principal_window means your key is over its ceiling — narrow its work or use a second key. tenant_window means the whole business is over the shared ceiling — run fewer things at once.

Retry-After is present only on rate-limit 429s. There is a second kind of 429: quota_exceeded, which means a plan allowance such as concurrent calls is used up. That response has no Retry-After, because when it clears depends on how long a live call runs, not on a fixed window. Read error.code to tell the two apart and apply your own back-off when the header is missing.

Error codes

Every error uses the same envelope: {"error": {"code": "...", "message": "..."}}. Some endpoints add a narrower reason. Branch on code and reason; treat message as text for a human.

Error codes and the HTTP status each maps to
CodeHTTPWhat it means
unauthenticated401No valid key or token in the Authorization header.
forbidden403Authenticated, but the key lacks the scope or role.
tenant_mismatch403That resource belongs to a different business account.
validation422The request parameters were rejected.
not_found404The resource does not exist.
conflict409The resource changed under you. Reload and retry.
rate_limited429A rate window is full. Retry-After is present.
quota_exceeded429A plan quota is exhausted. Retry-After is NOT present.
integration_unavailable503A connected integration is not reachable.
integration_timeout504A connected integration did not answer in time.
integration_rejected502A connected integration refused the request.
provider_unavailable503The voice provider is unavailable.
provider_session_lost503The voice session was lost. Start a new one.
internal500Something failed on our side.

One exception worth knowing before you write a sign-up flow: POST /v1/signup still answers a rate-limit refusal with a plain {"detail": "too_many_attempts"} body. Do not expect error.code there.

Webhooks

Instead of polling, register an HTTPS endpoint and we call it when something happens. You add endpoints in Settings → Integrations → Webhooks, or over the API.

The event catalogue lives on the server

Event names change as the product grows, so this page does not print a list that would quietly go stale. Ask the API for the current one:

curl https://api.officialaiagent.com/v1/webhooks/events \
  -H "Authorization: Bearer <JWT>"

The panel builds its event picker from exactly this response, so what you can subscribe to over the API and what you can tick in the panel can never disagree.

Verifying the signature

Every delivery carries an X-Fc-Signature header in the form t=<unix seconds>,v1=<hex>. The v1 value is an HMAC-SHA256 over <t>.<raw body>, keyed with the signing secret you were shown once when the endpoint was created.

import hashlib, hmac, time

SECRET = "whsec_..."          # shown once, when you create the endpoint
TOLERANCE_S = 300             # reject anything older than five minutes

def verify(raw_body: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts, got = parts.get("t", ""), parts.get("v1", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > TOLERANCE_S:
        return False                                   # replay window
    signed = ts.encode() + b"." + raw_body             # RAW body, never re-serialised
    want = hmac.new(SECRET.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, got)              # constant time

Three details matter. Sign the raw body — re-serialising the JSON changes the bytes and the signature will not match. Compare in constant time. And reject an old timestamp, so a captured delivery cannot be replayed at you later.

Retries and automatic disabling

A delivery that fails is retried with a growing delay. After enough consecutive failures we stop calling the endpoint and mark it disabled, with the reason visible in the panel. Answer quickly with a 2xx and do the real work afterwards; a slow handler looks the same to us as a broken one.

Full API reference

Every endpoint, parameter and response shape is published as an OpenAPI document. Browse the reference, or fetch the raw document:

curl https://officialaiagent.com/api/openapi.json

Getting help

Something missing, or an endpoint behaving differently from this page? Write to hello@officialaiagent.com and tell us what you were calling — a person reads it. Business owners can also leave a request from Settings → Integrations, which lands in the same place.