The Breadbox public API lets you read and write CRM data from any tool — PSA, RMM, automation platforms, custom workflows. Every call is scoped, audit-logged, and rate-limited.
Base URL
https://app.breadboxmsp.com/api/v1All endpoints are under /api/v1. Breaking changes ship as /api/v2 with a 6-month deprecation window. Generate keys in Settings → API & Webhooks.
OpenAPI 3.1
Pipe into openapi-generator, openapi-typescript, Stoplight, etc.
https://app.breadboxmsp.com/api/v1/openapi.jsonPostman / Insomnia / Bruno
One-click import — every endpoint pre-configured with examples.
https://app.breadboxmsp.com/api/v1/postman.jsonAuthentication
Every request requires a Bearer token in the Authorization header.
# curl example curl https://app.breadboxmsp.com/api/v1/accounts \ -H "Authorization: Bearer msp_live_xxxxxxxx"
Keys are prefixed msp_live_ for production and msp_test_ for test environments. Keys are SHA-256 hashed at rest — store them securely; the raw key cannot be retrieved after creation. Lost keys must be rotated.
Response envelope
Every response — success or error — uses a consistent JSON shape with a top-level success boolean and requestId (also returned as the X-Request-Id header — include it in any support ticket).
Single object
{
"success": true,
"data": { "id": "cuid_xxx", "name": "Acme Healthcare", ... },
"requestId": "req_a1b2c3d4e5f6g7h8",
"timestamp": "2026-05-06T18:42:11.443Z"
}Paginated list
{
"success": true,
"data": [ { "id": "cuid_xxx", ... }, { "id": "cuid_yyy", ... } ],
"hasMore": true,
"nextCursor": "cuid_yyy",
"requestId": "req_a1b2c3d4e5f6g7h8",
"timestamp": "2026-05-06T18:42:11.443Z"
}Error
{
"success": false,
"error": {
"code": "validation_error",
"message": "name must be at least 1 character",
"docsUrl": "https://app.breadboxmsp.com/docs/api#errors",
"details": [ { "path": ["name"], "message": "..." } ]
},
"requestId": "req_a1b2c3d4e5f6g7h8",
"timestamp": "2026-05-06T18:42:11.443Z"
}Scopes
Each API key is granted one or more scopes. A request fails with 403 insufficient_scope if the key lacks the required scope. Write scopes imply the corresponding read scope.
| Scope | What it grants |
|---|---|
accounts:read | Read accounts, sites, and account details |
accounts:write | Create, update, soft-delete accounts |
contacts:read | Read contacts and account-contact relationships |
contacts:write | Create and update contacts |
deals:read | Read pipeline deals and stages |
deals:write | Create deals, change stages, mark Won/Lost |
health:read | Read churn-risk scores and signal breakdowns |
health:write | Push churn-risk signals from external PSA/RMM tools |
touchpoints:read | Read touchpoints and activity timeline |
touchpoints:write | Log touchpoints from external tools |
devices:read | Read device inventory |
devices:write | Push and update device data from RMM tools (incl. bulk) |
tasks:read | Read tasks |
tasks:write | Create, update, complete, delete tasks |
notes:read | Read notes on accounts, deals, contacts, leads |
notes:write | Create notes on any supported object |
mrr:read | Read MRR data and financial summaries |
leads:read | Read leads and ICP scores |
leads:write | Create and update leads |
compliance:read | Read compliance items and framework status |
admin:read | Implies all :read scopes — for full read integrations |
Rate limits
Two-tier sliding window: per-key (prevents one integration hammering the API) and per-org (prevents using many keys to bypass per-key limits). Every response includes consumption headers.
| Plan | Per-key (req/min) | Org-wide (req/min) |
|---|---|---|
| Starter | 30 | 60 |
| Growth | 60 | 120 |
| Scale | 120 | 240 |
| Pro / Enterprise | 120-300 | 240-600 |
# Headers on every response X-RateLimit-Limit: 60 X-RateLimit-Remaining: 55 X-RateLimit-Reset: 1762470120 X-RateLimit-Policy: sliding-window Retry-After: 30 # only on 429 responses
Idempotency
Pass an Idempotency-Key header on POST/PATCH/DELETE requests to safely retry on network failure. Keys are stored for 24 hours; replaying the same key with the same body returns the original response without re-executing the handler. Replaying the same key with a different body returns 422 idempotency_conflict.
# Safe to retry indefinitely — same key + same body = same response curl -X POST https://app.breadboxmsp.com/api/v1/accounts \ -H "Authorization: Bearer msp_live_xxx" \ -H "Idempotency-Key: import-job-2026-05-06-abc123" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Healthcare" }'
Replays are returned with X-Idempotent-Replay: true so you can tell when a response came from cache. Idempotency-Key format: [a-zA-Z0-9_-]{1,255}— typically a UUID or your job ID. Don't reuse keys for different operations.
Pagination, sorting, filtering
All list endpoints use cursor-based pagination. Pass cursor from the previous response to fetch the next page. Default page size is 50, max 100.
# First page GET /api/v1/accounts?limit=50 # Next page (cursor from previous response's nextCursor) GET /api/v1/accounts?limit=50&cursor=cuid_xxxxx # Incremental sync — only records changed since timestamp GET /api/v1/devices?updatedAfter=2026-05-01T00:00:00Z # Sort — comma-separated field:direction pairs GET /api/v1/deals?sort=estimatedMrr:desc,createdAt:asc # Filter — endpoint-specific query params GET /api/v1/deals?stage=NEGOTIATION&ownerId=user_xyz&type=RENEWAL
When hasMore is false, you've reached the end. Sortable fields are restricted per endpoint — invalid fields are silently ignored (response uses the default sort). See the OpenAPI spec for each endpoint's allowed sort fields.
Endpoints
/api/v1/accountsaccounts:readList accounts (filter by lifecycle, industry; cursor pagination)/api/v1/accountsaccounts:writeCreate an account/api/v1/accounts/{id}accounts:readGet a single account/api/v1/accounts/{id}accounts:writeUpdate an account/api/v1/accounts/{id}accounts:writeSoft-delete (sets lifecycle to CHURNED). See ?force=true for hard delete./api/v1/contactscontacts:readList contacts (filter by accountId)/api/v1/contactscontacts:writeCreate a contact (optionally link to account)/api/v1/contacts/{id}contacts:readGet a single contact with account relationships/api/v1/contacts/{id}contacts:writeUpdate a contact/api/v1/dealsdeals:readList deals (filter by stage, accountId)/api/v1/dealsdeals:writeCreate a deal/api/v1/deals/{id}deals:readGet a single deal/api/v1/deals/{id}deals:writeUpdate a deal (including stage change and Won/Lost outcome)/api/v1/devicesdevices:readList devices (filter by accountId, isOnline, isEndOfLife, updatedAfter)/api/v1/devicesdevices:writeCreate a device/api/v1/devices/{id}devices:readGet a single device/api/v1/devices/{id}devices:writeUpdate a device (fires device.offline / end_of_life on transitions)/api/v1/devices/{id}devices:writeHard-delete a device/api/v1/devices/bulkdevices:writeBulk upsert devices by rmmAgentId (max 500/request)/api/v1/taskstasks:readList tasks (filter by status, assigneeId, accountId, dealId, updatedAfter)/api/v1/taskstasks:writeCreate a task/api/v1/tasks/{id}tasks:readGet a single task/api/v1/tasks/{id}tasks:writeUpdate a task (auto-stamps completedAt on status=DONE)/api/v1/tasks/{id}tasks:writeDelete a task/api/v1/notesnotes:readList notes (filter by objectType, objectId)/api/v1/notesnotes:writeCreate a note on Account, Deal, Contract, or Lead/api/v1/health-scoreshealth:readList accounts with their current churn risk/api/v1/health-scores/{accountId}health:readGet churn risk + signal breakdown + 90-day history/api/v1/health-scores/{accountId}/signalshealth:writePush an external churn-risk signal/api/v1/touchpointstouchpoints:readList touchpoints (filter by accountId, type, updatedAfter)/api/v1/touchpointstouchpoints:writeLog a touchpoint from an external tool/api/v1/mrrmrr:readOrg-level MRR summary by service category/api/v1/accounts/bulkaccounts:writeBulk upsert accounts (max 500/req, idempotent on externalId)/api/v1/contacts/bulkcontacts:writeBulk upsert contacts (max 500/req, idempotent on email)/api/v1/deals/bulkdeals:writeBulk create deals (max 500/req)Worked examples
Create an account, contact, and deal
# 1. Create the account curl -X POST https://app.breadboxmsp.com/api/v1/accounts \ -H "Authorization: Bearer msp_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Healthcare", "lifecycleStatus": "PROSPECT", "industry": "Healthcare", "employeeCount": 65 }' # Response { "success": true, "data": { "id": "cuid_acc_abc123", "name": "Acme Healthcare", "lifecycleStatus": "PROSPECT", ... }, "requestId": "req_xxx", "timestamp": "..." }
Push device inventory from your RMM (idempotent)
# Bulk upsert by rmmAgentId — re-sending the same agent IDs updates, doesn't duplicate curl -X POST https://app.breadboxmsp.com/api/v1/devices/bulk \ -H "Authorization: Bearer msp_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "devices": [ { "rmmAgentId": "ninja-12345", "accountId": "cuid_acc_xxx", "hostname": "WS-SMITH-01", "os": "Windows 11", "isOnline": true, "patchCompliant": true }, { "rmmAgentId": "ninja-12346", "accountId": "cuid_acc_xxx", "hostname": "WS-JONES-01", "os": "Windows 11", "isOnline": false } ] }' # Response { "success": true, "data": { "created": 1, "updated": 1, "total": 2, "results": [ { "rmmAgentId": "ninja-12345", "id": "cuid_dev_aaa", "action": "updated" }, { "rmmAgentId": "ninja-12346", "id": "cuid_dev_bbb", "action": "created" } ] }, ... }
Migrate accounts from another CRM (bulk + idempotent)
# Use externalId from your source CRM. Re-running the same payload is safe — same IDs update, new IDs create. curl -X POST https://app.breadboxmsp.com/api/v1/accounts/bulk \ -H "Authorization: Bearer msp_live_xxx" \ -H "Idempotency-Key: hubspot-migration-batch-001" \ -H "Content-Type: application/json" \ -d '{ "accounts": [ { "externalId": "hub-12345", "name": "Acme Healthcare", "industry": "Healthcare", "lifecycleStatus": "ACTIVE" }, { "externalId": "hub-12346", "name": "BluePeak Legal", "industry": "Legal", "lifecycleStatus": "AT_RISK" } ] }'
Log a touchpoint from an external tool
curl -X POST https://app.breadboxmsp.com/api/v1/touchpoints \ -H "Authorization: Bearer msp_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "accountId": "cuid_acc_xxx", "type": "EMAIL", "direction": "OUTBOUND", "subject": "Renewal terms", "notes": "Sent draft contract to Sarah" }'
Error codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | bad_request | Missing or malformed path parameter |
| 400 | invalid_json | Request body is not valid JSON |
| 401 | unauthorized | Missing or invalid API key |
| 403 | insufficient_scope | Key lacks the required scope for this endpoint |
| 404 | not_found | Record not found, or it belongs to another org |
| 405 | method_not_allowed | HTTP method not supported on this endpoint |
| 422 | validation_error | Request body failed schema validation. See error.details. |
| 422 | idempotency_conflict | Idempotency-Key reused with a different request body |
| 429 | rate_limited | Rate limit exceeded — see Retry-After header |
| 500 | internal_error | Unexpected server error — retry with exponential backoff |
Webhooks
Breadbox pushes real-time events to your endpoints as data changes. Configure endpoints in Settings → API & Webhooks. Each endpoint subscribes to specific events (or * for all). Failed deliveries retry with exponential backoff up to 5 attempts; an endpoint that fails 5 consecutive times is auto-disabled.
Payload envelope
{
"id": "evt_1762470120_a1b2c3d4",
"event": "deal.closed_won",
"timestamp": "2026-05-06T18:42:11.443Z",
"orgId": "org_xxxxx",
"data": { /* the same shape as the corresponding GET response */ }
}Available events
| account.created | An account was created via the API or UI |
| account.updated | Any field on an account changed |
| account.deleted | Account soft-deleted (lifecycle → CHURNED) |
| contact.created | A contact was created |
| contact.updated | A contact was updated |
| deal.created | A new deal entered the pipeline |
| deal.stage_changed | A deal moved between stages (excluding Won/Lost) |
| deal.closed_won | A deal was marked Closed Won |
| deal.closed_lost | A deal was marked Closed Lost |
| device.created | A device was added to inventory |
| device.updated | A device's fields changed |
| device.offline | A device transitioned from online to offline |
| device.end_of_life | A device was flagged end-of-life |
| touchpoint.created | A touchpoint was logged |
Headers on every webhook
Breadbox-Signature: t=1762470120,v1=a3f2b1c4... Breadbox-Event: deal.closed_won Breadbox-Event-Id: evt_1762470120_abc123 Content-Type: application/json User-Agent: Breadbox-Webhooks/1.1
Verifying signatures (recommended)
The Breadbox-Signature header carries a Unix timestamp and a v1 HMAC-SHA256 of <timestamp>.<raw_body>. Verify both the signature AND that the timestamp is recent (within 5 minutes) to mitigate replay attacks.
// Node.js / TypeScript
import { createHmac, timingSafeEqual } from 'crypto';
const TOLERANCE_SECONDS = 5 * 60;
function verifyWebhook(rawBody: string, header: string, secret: string): boolean {
// Parse "t=<unix_seconds>,v1=<hmac_hex>"
const parts = Object.fromEntries(
header.split(',').map((p) => {
const [k, v] = p.split('=');
return [k, v];
})
);
const ts = parseInt(parts.t ?? '', 10);
const sig = parts.v1 ?? '';
if (!ts || !sig) return false;
// Reject stale events (replay protection)
const ageSec = Math.abs(Date.now() / 1000 - ts);
if (ageSec > TOLERANCE_SECONDS) return false;
const expected = createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
if (sig.length !== expected.length) return false;
return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
// Express handler
app.post('/webhooks/breadbox', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.header('Breadbox-Signature') ?? '';
if (!verifyWebhook(req.body.toString(), sig, process.env.BREADBOX_WEBHOOK_SECRET!)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString());
// ... handle event.event, event.data
res.status(200).end();
});Legacy X-MSP-Signature / X-MSP-Timestamp headers (HMAC of just the raw body) are still sent for backwards compatibility but will be removed in a future release. Migrate to Breadbox-Signature for replay protection.
Test mode
Keys prefixed msp_test_ operate against the same data as live keys but are flagged in audit logs as test traffic. Test keys do not fire outbound webhooks — useful for development without flooding your real endpoints. Full sandbox isolation (separate test database) ships in a future release.
SDK examples
The API follows standard REST conventions — any HTTP client works. Official SDKs (TypeScript, Python) are on the roadmap. Quickstarts:
Python (requests)
import requests
BASE = "https://app.breadboxmsp.com/api/v1"
HEADERS = {"Authorization": "Bearer msp_live_xxxxx"}
# List accounts
resp = requests.get(f"{BASE}/accounts?limit=50", headers=HEADERS)
resp.raise_for_status()
body = resp.json()
assert body["success"] is True
accounts = body["data"]
# Create a deal
resp = requests.post(f"{BASE}/deals", headers=HEADERS, json={
"name": "Acme Corp — Managed IT",
"accountId": "cuid_xxxxx",
"estimatedMrr": 3500,
"probability": 60,
})
deal = resp.json()["data"]PowerShell (for MSP automation)
$headers = @{ Authorization = "Bearer msp_live_xxxxx" }
$base = "https://app.breadboxmsp.com/api/v1"
# Push device data from RMM
$body = @{
rmmAgentId = "ninja-12345"
accountId = "cuid_xxxxx"
hostname = "WS-SMITH-01"
deviceType = "Workstation"
os = "Windows 11"
} | ConvertTo-Json
Invoke-RestMethod -Uri "$base/devices" -Method POST `
-Headers $headers -Body $body -ContentType "application/json"TypeScript (fetch)
const BASE = "https://app.breadboxmsp.com/api/v1";
const KEY = process.env.BREADBOX_API_KEY!;
async function api<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
...init,
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
...(init?.headers ?? {}),
},
});
const body = await res.json();
if (!body.success) throw new Error(body.error?.message ?? "API error");
return body.data as T;
}
// Usage
const accounts = await api<Account[]>("/accounts?limit=50");
const deal = await api<Deal>("/deals", {
method: "POST",
body: JSON.stringify({ name: "Acme renewal", accountId: "cuid_xxx", estimatedMrr: 3500 }),
});Need help?
API questions, feature requests, and bug reports: api@breadboxmsp.com. Always include the requestId from a recent failing call — it lets us trace your exact request in our logs.