Calibri public API
https://api.getcalibri.com/api/public/v1https://api.getcalibri.com/mcpWhat this is
An HTTP API and an MCP endpoint for the deliverability data Calibri measures for your workspace: per-mailbox scores, inbox placement per provider (how much lands in spam), domain health, alerts and the weekly report. Your scripts, n8n, Make, Sheets and Slack read it over HTTP; Claude, Claude Code and Cursor read it over MCP. Over MCP, an assistant can also switch weekly inbox placement tests on or off and connect Slack, if the workspace owner allowed changes for its key.
- REST base:
https://api.getcalibri.com/api/public/v1 - MCP:
https://api.getcalibri.com/mcp - Every response is JSON, except this page (markdown).
- The REST API never writes. A key reads one workspace and nothing else.
- Three MCP tools make changes (
set_placement_tests,connect_slack,disconnect_slack). They work only with a key the workspace owner allowed to make changes; every other key stays read-only. - Scores are cached, not recomputed on request: mailbox scores refresh twice a day, domain health hourly. Every score carries
computedAt, so you can see how fresh it is. Polling more often than that returns the same numbers. The exception is the weekly report: it is rebuilt on a read when the cached copy no longer matches the workspace (for example a mailbox or domain was added), so two reads can differ.
GET /api/public/v1 (no key) lists the endpoints. GET /api/public/v1/docs (no key) returns this page.
Authentication
Send your key in the Authorization header on every request:
Authorization: Bearer cal_live_<48 hex characters>
- Create, view and revoke keys in Calibri under Connectors > API & MCP (workspace owners). A key is shown once, at creation. Store it like a password.
- A key belongs to one workspace. Keys start with
cal_live_. - Every key is read-only unless the owner turns on "Allow changes" for it (at creation or later, under Connectors > API & MCP). Only the three MCP write tools use that permission. A key with changes allowed can switch placement tests and point the workspace's Slack alerts at any Slack channel, so give it only to tools you trust. Turning it off takes effect on the key's next request. Every change made with a key is logged with the key's id and label.
- Send the key only in the header. Never put it in a URL or query string: URLs end up in server logs. Calibri does not accept keys in URLs.
- Keys for the Calibri dashboard do not work here, and public keys do not work on the dashboard API.
- The workspace needs an active subscription, otherwise requests return 402.
A missing, invalid or revoked key returns 401 with a WWW-Authenticate: Bearer realm="calibri" header:
{ "error": "unauthorized", "message": "This key is invalid or was revoked." }A request with no usable Authorization header returns the message Send Authorization: Bearer cal_live_... Create a key in Calibri under Connectors > API & MCP.
Rate limits
- 120 requests per minute per key. Over the limit: 429 with a
retry-afterheader (seconds). - 600 requests per minute per IP address, on top of the per-key limit.
GET /reports/weekly(and theget_weekly_reportMCP tool) is also limited to 200 reads per workspace per day. Over it: 429rate_limited.- The
connect_slackMCP tool is limited to 20 attempts per workspace per day (each attempt posts a test message to Slack). Over it: a tool error.
Because data refreshes at most hourly, one call every few minutes is plenty.
Endpoints
All paths are relative to https://api.getcalibri.com/api/public/v1. Email addresses and domain names in paths are trimmed and case-insensitive. Put @ in a path as %40 if your client does not do it for you.
GET/workspace
The workspace this key belongs to, and the key in use.
curl -s https://api.getcalibri.com/api/public/v1/workspace \
-H "Authorization: Bearer $CALIBRI_KEY"Fields: id, name, timezone (string or null), plan, key: { id, label, keyPrefix, canWrite }. canWrite is true when the owner allowed changes for this key.
GET/mailboxes
All mailboxes, ordered by email.
curl -s "https://api.getcalibri.com/api/public/v1/mailboxes?domain=example.com" \
-H "Authorization: Bearer $CALIBRI_KEY"Query parameters:
domain(optional): only mailboxes on that domain.includeRevoked(optional,trueorfalse, defaultfalse): revoked mailboxes are hidden from the list unless you passtrue. A revoked mailbox can still be read by address.
Returns { mailboxes: Mailbox[], count }. A Mailbox has:
id,email,domain,provider,status.ageBucket:lt_1m,1_3m,4_6m,gt_6mor null.score: see the glossary. A mailbox with no cached score yet hasscore.status = "collecting".trend:{ delta7d, delta30d, direction }where direction isimproving,declining,flatorinsufficient_history. Null while collecting.warmup:{ status, progressPct }, statusnot_warmed_up,warming_uporwarmed_up. Null while collecting.metrics(null while collecting):windowDays(always 14),sends24h,sends7d,sendsAll,sendsCold,sendsWarmup,bounces,bounceRatePct,hardBounceRatePct,coldBounceRatePct,warmupBounceRatePct,blocklistListings,senderAuth(pass,fail,unknown). Bounce figures cover the last 14 days and every bounce counts, hard and soft.quotaToday(or null):{ newToday, totalToday, status }, statusok,warn,at_caporover_cap.placement: array of placement rows (see the glossary), one per provider tested.
GET/mailboxes/:email
One mailbox, same shape as above. 404 not_found if the address is not in this workspace.
GET/mailboxes/:email/history
Daily facts for one mailbox.
curl -s "https://api.getcalibri.com/api/public/v1/mailboxes/ana%40example.com/history?days=30" \
-H "Authorization: Bearer $CALIBRI_KEY"days: integer from 1 to 90, default 30. Returns { entity: { type, id, label }, days, rows }, with rows ascending by day, each { day, version, facts }. facts is an object of that day's measurements; its keys can grow over time, so read the ones you need and ignore the rest.
GET/domains
All monitored domains, ordered by name. Returns { domains: Domain[], count }.
curl -s https://api.getcalibri.com/api/public/v1/domains \
-H "Authorization: Bearer $CALIBRI_KEY"A Domain has:
id,fqdn,status,provider(google,microsoft,mixedor null),ageBucket.placementTestsEnabled: true when weekly inbox placement tests are on for this domain. Read live, not from the score cache.score: as for mailboxes, pluscollectingReason(warming,no_dataor null). A domain'sscore.statusis the band of its score, the colour the Domains page shows. One weak inbox shows on that mailbox's ownscore.status, not on the domain.mailboxes:{ total, active, warmedUp }(or null).sends24h(or null).bounces(or null):windowDays(14),coldRatePct,warmupRatePct,combinedRatePct,coldSends,warmupSends,coldSampleMeaningful,warmupSampleMeaningful. A rate is null when its sample is too small to judge (fewer than 50 sends in the window and fewer than 10 bounces), the same gate the Domains page uses; the sends count is always there.auth(or null):auditedAt,score,spfPresent,spfLookupCount,dkimState,dmarcPresent,dmarcPolicy,dmarcPct,mxPresent,customTrackingState,websiteRedirectVerdict.dmarc7d(or null):directMessages,directSpfPct,directDkimPct,forwardedMessages.blocklist(or null):activeListings,listings: [{ list, listedAt }].postmaster(or null):statsDate,reputation(HIGH,MEDIUM,LOW,BADor null),userReportedSpamRatio. Google Postmaster data, when available.placement: placement rows.recommendations:[{ severity: high|medium|low, signal, message }].
GET/domains/:fqdn
One domain, same shape. 404 not_found if it is not in this workspace.
GET/domains/:fqdn/placement
Recent completed inbox placement tests for a domain, newest first.
curl -s "https://api.getcalibri.com/api/public/v1/domains/example.com/placement?limit=10" \
-H "Authorization: Bearer $CALIBRI_KEY"limit: integer from 1 to 50, default 10. Returns { fqdn, tests }. Each test: id, completedAt, trigger, senderEmail (or null) and results, an object keyed by provider (google, ms365; a provider is absent if it was not tested). Each result has total, inbox, spam, tab, filed, missing, band, inboxPct, spamPct.
GET/domains/:fqdn/history
Daily facts for one domain. Same parameters and shape as the mailbox history (days 1 to 90, default 30).
GET/alerts
Alerts fired in a window, newest first.
curl -s "https://api.getcalibri.com/api/public/v1/alerts?since=7d&severity=critical,high" \
-H "Authorization: Bearer $CALIBRI_KEY"Query parameters:
since(default7d):24h,7d(any number of hours or days) or an ISO date or timestamp. At most 30 days back.severity: comma-separated list ofcritical,high,medium,low,info.kind: comma-separated list of alert kinds, at most 10.domain: only alerts for that domain.acknowledged:trueorfalse.limit: integer from 1 to 200, default 50.
Returns { alerts, count, window: { since, now } }. Each alert: id, kind, severity, title, body (or null), domain (or null), firedAt, acknowledgedAt (or null). body is always null for bounce_classified alerts, whose text quotes raw bounce messages; the title still says what happened.
GET/reports/weekly
The workspace report the Calibri app renders: score, domain and mailbox totals, volume, movers and findings.
curl -s "https://api.getcalibri.com/api/public/v1/reports/weekly?period=week" \
-H "Authorization: Bearer $CALIBRI_KEY"period: week or month, default month. The response is the report object, including tenant (id, name), period (start, end, days, label), score, domains, mailboxes, infrastructure and volume. Its summary field is the AI summary object (the same as aiSummary), not a string. Limited to 200 reads per workspace per day.
Field glossary
score: { composite, grade, status, computedAt }
composite: 0 to 100, or null when there is no score.grade:A(85 and up),B(70 to 84),C(55 to 69),D(40 to 54),F(below 40), orunknown.status:healthy,at_risk,action_required,burnt,insufficient_dataorcollecting.collecting: Calibri has not cached a score for this mailbox or domain yet (new, or its first cycle has not run). It is returned, not omitted.compositeis null andgradeisunknown. Check again later.insufficient_data: scored, but there is too little sending evidence to judge.- For a domain,
statusfollows the score band:healthyat 85 and up,at_riskfrom 70 to 84,action_requiredbelow 70. A mailbox'sstatusis its own evidence, so a single weak inbox can sit under a healthy domain.
computedAt: ISO timestamp of the cached score, or null while collecting. Mailbox scores refresh twice a day; domain health refreshes hourly.
placement row: { provider, band, testedAt, ageDays, stale, counts, inboxPct, spamPct }
provider:google(Gmail seed inboxes) orms365(Outlook seed inboxes).band:inboxing,mixed,spam_heavyorinconclusive.counts:total,inbox,spam,filed,missing, andtabwhen present (Gmail promotions or updates tabs).inboxPctandspamPct: share oftotalthat landed in the inbox or in spam, with one decimal. Null whentotalis 0.ageDays: age of the test.staleis true when the test is older than 14 days.
Bounce metrics cover the last 14 days (windowDays: 14). Every bounce counts, hard and soft. bounceRatePct is bounces over sends in that window. On a domain, coldRatePct and warmupRatePct are null when the sample is too small (fewer than 50 sends and fewer than 10 bounces); coldSampleMeaningful and warmupSampleMeaningful say which.
Weekly report bands: domains.critical in /reports/weekly is the same band as action_required on a domain's score.status.
Timestamps are ISO 8601 in UTC. Days (day, statsDate) are YYYY-MM-DD.
MCP
Connect an AI assistant to the same data.
- URL:
https://api.getcalibri.com/mcp - Transport: Streamable HTTP, stateless (POST only; GET and DELETE return 405).
- Auth: the same
Authorization: Bearer cal_live_...header. Same rate limits as the REST API. - One JSON-RPC message per POST: batches (a JSON array) get 400 with error code
-32600. Request bodies over 1 MiB get 413. - Server name:
calibri. Ten tools read; three make changes and need a key with changes allowed.
Tools:
| Tool | Arguments |
|---|---|
get_workspace | none |
list_mailboxes | domain?, includeRevoked? |
get_mailbox | email |
list_domains | none |
get_domain | fqdn |
get_placement_history | fqdn, limit? (1 to 50, default 10) |
list_alerts | since?, severity? (array), kind? (array), domain?, acknowledged?, limit? (1 to 200, default 50) |
get_weekly_report | period? (week or month, default month) |
get_history | entity (mailbox or domain), id (email or domain name), days? (1 to 90, default 30) |
get_slack_setup | none |
set_placement_tests (changes) | domain, enabled (boolean) |
connect_slack (changes) | webhookUrl |
disconnect_slack (changes) | none |
Each read tool returns the REST shape as structuredContent and as JSON text. Not found and bad input come back as tool errors (isError: true).
Tools that make changes or set up Slack:
get_slack_setup(read-only, any key):{ connected, muted, alertsToSlack: { enabled, minSeverity, categories }, weeklyReportToSlack, settingsUrl, steps }.stepsexplains how to connect Slack: from the Slack card on the Connectors page, or by creating a Slack incoming webhook (api.slack.com/apps > Create New App > From scratch > Incoming Webhooks > On > Add New Webhook to Workspace > pick a channel > copy the URL) and giving it toconnect_slack.set_placement_tests: switches weekly inbox placement tests on or off for one domain, the same as the toggle on the Domains page. While on, Calibri sends real probe mail every week to Gmail and Outlook seed inboxes. Paid plans only: turning tests on in a Free workspace returns the errorInbox placement tests are on paid plans.Turning them off always works; a test already running still finishes. Returns{ domain, placementTestsEnabled, changed }. Safe to repeat.connect_slack: takes a Slack incoming webhook URL (https://hooks.slack.com/..., no other host is accepted). Calibri postsCalibri is connected. Deliverability alerts will post here.to the channel and saves the webhook only if Slack accepts it. It replaces any existing Slack connection; the previous channel gets a short notice that alerts moved, naming the key. Which alerts and reports go to Slack is still chosen in Calibri's Alerts settings; the result shows the current choice (alertsToSlack,weeklyReportToSlack). The URL is stored encrypted and never returned.disconnect_slack: posts a short goodbye in the channel, then removes the Slack connection, the same as Disconnect on the Connectors page. Returns{ connected: false, wasConnected }. Safe to repeat.
With a read-only key the three write tools return the error This key is read-only. The workspace owner can allow changes for this key in Connectors > API & MCP.
Connect:
- Claude (claude.ai and desktop): Settings > Connectors > Add custom connector. URL
https://api.getcalibri.com/mcp. Under Request headers addAuthorizationwith the valueBearer <your key>. - Claude Code:
bash
claude mcp add --transport http calibri https://api.getcalibri.com/mcp --header "Authorization: Bearer <your key>" - Cursor: add to
mcp.json:json{ "mcpServers": { "calibri": { "url": "https://api.getcalibri.com/mcp", "headers": { "Authorization": "Bearer <your key>" } } } }
ChatGPT connectors need OAuth, which Calibri does not offer yet.
Errors
Errors are JSON: { "error": "<code>", "message": "<what to do>" }.
| Status | error | When |
|---|---|---|
| 400 | bad_request | A parameter is invalid. The message names it, for example limit must be an integer from 1 to 50. |
| 401 | unauthorized | Missing, invalid or revoked key. Includes WWW-Authenticate. |
| 402 | subscription_required | The workspace has no active subscription. The body is exactly { error, message }. |
| 404 | not_found | No such mailbox or domain in this workspace. |
| 429 | rate_limited | Over 120 requests per minute per key, 600 per minute per IP, or 200 weekly report reads per day. Honour retry-after. |
Changelog
- v1, 2026-11: first release. Ten read-only endpoints and nine MCP tools.
- 2026-11-11: four MCP tools:
get_slack_setup, and three that make changes with a key the owner allowed to (set_placement_tests,connect_slack,disconnect_slack). Domains gainplacementTestsEnabled; the workspacekeygainscanWrite. The REST API stays read-only. - 2026-11-02: a domain's
score.statusis the band of its score, the same as the Domains page. Before, it was the worst mailbox's status, so an A-graded domain could readat_risk. - 2026-11-02: domain bounce rates are null when the sample is too small, with
coldSampleMeaningfulandwarmupSampleMeaningfuladded, the same gate as the Domains page. Before, a domain with 13 cold sends could show a 277% cold bounce rate.