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.
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.
| Tool | Scope | What it is |
|---|---|---|
get_overview | config:read | Read only |
get_manager_brief | calls:read | Read only |
analyze_call | calls:write | Writes to your business |
list_services | config:read | Read only |
list_models | config:read | Read only |
list_closures | config:read | Read only |
list_recent_calls | calls:read | Read only |
update_setting | config:write | Writes to your business |
add_closure | config:write | Writes to your business |
remove_closure | config:write | Writes to your business |
add_service | config:write | Writes to your business |
update_service | config:write | Writes to your business |
save_knowledge | kb:write (also accepts config:write during the migration window) | Writes to your business |
list_bookings | calls:read | Read only |
list_open_slots | calls:read | Read only |
move_booking | calls:write | Writes to your business |
request_capability | config:write | Writes to your business |
list_capability_requests | config:read | Read only |
get_capability_request | config:read | Read only |
submit_capability_feedback | config:write | Writes to your business |
list_team_day | calls:read | Read only |
list_tasks | config:read | Read only |
complete_task | config:write | Writes to your business |
get_attention | calls:read | Read only |
get_usage | calls:read | Read only |
get_setup_gaps | config:read | Read only |
list_action_plans | config:read | Read only |
get_revenue | calls:read | Read only |
get_occupancy | calls:read | Read only |
get_call_quality | calls:read | Read only |
get_human_requests | calls:read | Read only |
get_customer_rhythm | calls:read | Read only |
list_staff_agenda | calls:read | Read only |
list_findings | calls:read | Read only |
list_webhooks | config:read | Read only |
dismiss_finding | calls:write | Writes to your business |
draft_action_plan | config:write | Writes to your business |
add_staff | config:write | Writes to your business |
update_staff | config:write | Writes to your business |
assign_task | config:write | Writes to your business |
create_webhook | config:write | Writes 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.
| Scope | Read-only key | Opens |
|---|---|---|
config:read | yes | Assistant settings, services, closures, knowledge map |
config:write | no | Write settings, services and closures |
calls:read | yes | Calls and chats: list, detail, transcript, callers |
calls:write | no | Bookings and action items |
kb:read | yes | Knowledge base documents: list, content, preview |
kb:write | no | Write to and delete from the knowledge base |
s2s:mint | never issued | The 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.
| Lane | Per business | Per key or user |
|---|---|---|
| Reads (list, detail) | 300 / min | 120 / min |
| Writes (POST, PATCH, DELETE) | 120 / min | 60 / min |
| Analytics (/v1/analytics*) | 60 / min | 30 / min |
Endpoints that spend money or write to a ledger have their own hourly bucket on top of the per-minute lane:
| Endpoint | Per business | Per key or user |
|---|---|---|
POST /v1/kb/ingest/commit/* | 30 / hour | — |
POST /v1/manager/diagnostics/calls/{id} | 60 / hour | 30 / hour |
POST /v1/manager/action-plans/{id}/execute | 30 / hour | 15 / hour |
POST /v1/api-keys | 10 / hour | — |
/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.
| Code | HTTP | What it means |
|---|---|---|
unauthenticated | 401 | No valid key or token in the Authorization header. |
forbidden | 403 | Authenticated, but the key lacks the scope or role. |
tenant_mismatch | 403 | That resource belongs to a different business account. |
validation | 422 | The request parameters were rejected. |
not_found | 404 | The resource does not exist. |
conflict | 409 | The resource changed under you. Reload and retry. |
rate_limited | 429 | A rate window is full. Retry-After is present. |
quota_exceeded | 429 | A plan quota is exhausted. Retry-After is NOT present. |
integration_unavailable | 503 | A connected integration is not reachable. |
integration_timeout | 504 | A connected integration did not answer in time. |
integration_rejected | 502 | A connected integration refused the request. |
provider_unavailable | 503 | The voice provider is unavailable. |
provider_session_lost | 503 | The voice session was lost. Start a new one. |
internal | 500 | Something 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 timeThree 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.