Auth is the X-API-Key header. Writes take dry_run (default false; writes are real). Import the OpenAPI file. This page is not the spec.
OpenAPI 0.100.0. Alert catalog, send, test, routing, snooze, and billing CSV bodies are typed. Field names are the schema titles on Alerts and What we write.
Billing quantities: POST /tenants/{tenant_id}/billing/quantities. The MSP matches the line. You send an absolute quantity (0 stops billing) on your cadence (daily or monthly). The MSP's change limit defaults to 25%. A change over that limit waits. Ending a line is refused. Each row takes optional trial_end_date, an ISO date YYYY-MM-DD or null. Trial ending, Trial converted, and Trial expired use it. Omit it and the stored date stays. A bad date fails that row.
The request matches BillingQuantitiesBody. trial_end_date is optional on each row and this example omits it. The response is the sandbox body for that request when the customer is not matched yet. The 200 schema is an object.
SDK 2.0.0 (breaking): the generated Python and TypeScript clients now return typed response models where the spec declares one (for example TicketList) instead of plain dicts. Read attributes (listed.sandbox) or call .to_dict().
GET|PUT /isv/experience-config sets category config for PSA, RMM, Docs, CS, and Commerce, plus recommended grants and branding. Vendors inherit the category template. Access is List, Read, and Write, and those stack. Never is its own setting. The MSP can tighten the starting verbs. Sandbox Experience Preview walks Connect after Save: keys, grants, boards, map, done. Getting Started on Setup is Suggest, Save, Preview, then Use MCP or Use API. After Save, copy-paste prompts follow the saved profile. Partner credentials and Production MSP Connect keys are separate. Connections lists each MSP and vendor, last sync, and sync detail. Issues gives a support blurb and a redacted request debug.
Halo and ConnectWise company custom fields: POST /tenants/{tenant_id}/companies/write writes a value on an existing company with vendor_native, and ensure_custom_field creates the missing field definition. The field works live on Halo and ConnectWise. It is created once in your stack under the MSP's standing psa.company.field.create grant, and an existing field with the same label is reused. The field label shows on Configure. Value writes use the psa.company.update grant, off by default. A missing grant is grant_missing. If the update does not run, the code is psa_company_update_refused.
Machine API auth is X-API-Key. Console login is a magic link or Sign in with Microsoft, for an email that is already seated or invited.
GET /tenants/{tenant_id}/tickets
POST /tenants/{tenant_id}/tickets
Tickets
Tickets are two-way when the MSP grants it. You can create, update, and close. You can subscribe to events when the MSP changes the ticket in their PSA. Grants: psa.ticket.create, psa.ticket.update, psa.ticket.events. When a ticket is created, updated, noted, closed, or reopened, you can list those events or get a signed webhook. After a tenant sync, GET /tenants/{id}/connections/events?kind=ticket.closed (also created, updated, noted, reopened). Optional signed webhook. Grant is the connection (psa.ticket.events). Missing is grant_missing. When a device is created or updated, you can list those events or get a signed webhook. After a tenant sync, GET /tenants/{id}/connections/events?kind=device.updated (also created). Optional signed webhook. Grant is the connection (rmm.device.events). Missing is grant_missing. When a company is created or updated, you can list those events or get a signed webhook. After a tenant sync, GET /tenants/{id}/connections/events?kind=company.updated (also created). Optional signed webhook. Grant is the connection (psa.company.events). Missing is grant_missing. You can close a ticket once they accept the connection. Auto-close when a finding is fixed is a separate switch. Hours are a write, not a read. Required member is the picker id from GET /tenants/{tenant_id}/members. Create and update accept optional board, status, and priority list ids (GET …/ticket-boards, ticket-statuses, ticket-priorities on ConnectWise and Halo); unknown id → 422. Writes are real by default.
Absolute quantities on lines the MSP matched. Quantity 0 stops billing. A change over the MSP's limit (default 25%) waits. Ending a line is refused. Optional trial_end_date on each row, ISO YYYY-MM-DD or null. See What we write.
POST
/tenants/{tenant_id}/billing/quantities/csv
Request is BillingQuantityCsvBody. The route description says the result matches the JSON quantities call. An empty file is 422 request_invalid, "The file is empty." A header with no rows is "The file has no quantities."
GET / PUT
/isv/billing-services
Billing service catalog. sent is daily or monthly. Sandbox PUT requires catalog_token from the GET. Duplicate key message: Two billing services use the key '{id}'.
GET / PUT
/isv/alert-types
Alert-type catalog. GET returns AlertTypeList. PUT body is AlertTypesBody. PUT 200 is AlertTypeWriteResult. Same body as the tenant route. See Alerts.
POST
/tenants/{tenant_id}/alerts
Body AlertSendBody. 200 is AlertSendResult. Always send event_id or idempotency_key. With dedupe on, the first create for a device still commits without them. A note, a create with dedupe off, or a repeat create with both omitted is 422 request_invalid. Alerts off: 409 feature_off.
POST
/tenants/{tenant_id}/alerts/test
Body AlertTestBody or null. 200 is AlertSendResult. No type uses the first catalog type. Sandbox {} used subscription-renewal. Title was [Bitlync test] CrowdStrike: no action required, safe to close. priority_name was Informational.
POST
/isv/alert-types/import
Body {"csv": "..."}. Columns are name, description, type, and severity. Nothing is saved. The sandbox 200 was draft true, count, and draft rows. The type column came back as a tag. Category was Product alerts. No name column is 422 "The CSV needs a name column."
GET
/tenants/{tenant_id}/alert-routing
Returns AlertRoutingView. Critical and High go to Ticket. Medium goes to Email. Low and Info go to Daily digest. The MSP can set its own severity per alert type, and routing uses that band. Digests are not sent in production today: a digest row is delivered at send time.
PUT
/tenants/{tenant_id}/alert-routing
Alert routing is set by the MSP in Connect. This PUT saves Send to when no destination is stored, and updates or resets a destination this vendor saved. When the MSP has saved the destination in Connect, the call returns 409 routing_msp_owned and "The MSP sets alert routing for this tenant in Connect. Nothing was saved." The stored row is unchanged. Body is AlertRoutingBody. 200 is AlertRoutingView. See Alerts.
GET / POST
/tenants/{tenant_id}/alert-snoozes
GET returns SnoozeList. POST body is SnoozeCreateBody. A target is required even though the schema marks target and target_ref optional. That is a known schema bug. duration is 24h, 7d, or until_resolved. 200 is SnoozeCreated.
Time picker. Halo and ConnectWise when the member permission is on. A missing ConnectWise members permission returns 502 upstream_auth_failure. Each id is what CreateTimeEntryBody.member accepts.
GET
/tenants/{tenant_id}/ticket-boards
Board ids for ConnectWise and Halo (ConnectWise board, Halo team).
GET
/tenants/{tenant_id}/ticket-statuses
Status ids for create and update on ConnectWise and Halo. An unknown id returns 422.
GET
/tenants/{tenant_id}/ticket-priorities
Priority ids for create and update on ConnectWise and Halo. An unknown id returns 422.
GET
/tenants/{tenant_id}/devices
Store-served device list.
GET
/tenants/{tenant_id}/applications
GET
/tenants/{tenant_id}/audit
GET
/tenants/{tenant_id}/connections
GET
/tenants/{tenant_id}/connections/events
GET
/tenants/{tenant_id}/identity/mappings
GET
/tenants/{tenant_id}/identity/unmapped
GET
/tenants/{tenant_id}/incidents
GET
/tenants/{tenant_id}/status
GET / PUT
/tenants/{tenant_id}/routing
POST
/tenants/{tenant_id}/companies
PSA company create is not supported: companies are created by the MSP. A PSA create returns 403 company_create_not_supported with no PSA call. Match to an existing company. Distributor create (Pax8, fan_out=["commerce"]) is unchanged.
POST
/tenants/{tenant_id}/companies/write
A Halo or ConnectWise company custom-field value on an existing company, via vendor_native, with ensure_custom_field creating the missing field definition. The field works live on Halo and ConnectWise. It is created once in your stack under the MSP's standing psa.company.field.create grant, and an existing field with the same label is reused. The field label shows on Configure. Value writes use the psa.company.update grant, off by default. A missing grant is grant_missing.
POST
/tenants/{tenant_id}/findings/route
GET / PUT
/isv/experience-config
Category config, recommended grants, and branding. Vendors inherit the category template. Access is List, Read, and Write. Preview walks Connect after Save.
POST
/sandbox/reset
Sandbox tenants only
Ticket
GET and POST /tenants/{tenant_id}/tickets. Tickets are two-way when the MSP grants it. You can create, update, and close. You can subscribe to events when the MSP changes the ticket in their PSA. When a ticket is created, updated, noted, closed, or reopened, you can list those events or get a signed webhook. After a tenant sync, GET /tenants/{id}/connections/events?kind=ticket.closed (also created, updated, noted, reopened). Optional signed webhook. Grant is the connection (psa.ticket.events). Missing is grant_missing. When a device is created or updated, you can list those events or get a signed webhook. After a tenant sync, GET /tenants/{id}/connections/events?kind=device.updated (also created). Optional signed webhook. Grant is the connection (rmm.device.events). Missing is grant_missing. When a company is created or updated, you can list those events or get a signed webhook. After a tenant sync, GET /tenants/{id}/connections/events?kind=company.updated (also created). Optional signed webhook. Grant is the connection (psa.company.events). Missing is grant_missing. You can close a ticket once they accept the connection. Auto-close when a finding is fixed is a separate switch. Time takes hours and a required member. The member is the picker id from GET /tenants/{tenant_id}/members. Board, status, and priority on create and update take ids from the ConnectWise and Halo lists. An unknown id returns 422.
Company
POST /tenants/{tenant_id}/companies. Pax8 matches a company it already has, then creates the rest. PSA company create is not supported: companies are created by the MSP, and a PSA create returns 403 company_create_not_supported. Exact-name auto-Match fills Match. Ambiguous names stay Needs match. Each unmatched row is Match, company import from PSA, or Skip. Company import creates the customer in the ISV after a dry_run preview. Contacts are list and match. POST /tenants/{tenant_id}/companies/write writes a Halo or ConnectWise company custom-field value on an existing company via vendor_native, and ensure_custom_field creates the missing field definition. The field works live on Halo and ConnectWise. It is created once in your stack under the MSP's standing psa.company.field.create grant, and an existing field with the same label is reused. The field label shows on Configure. Value writes use the psa.company.update grant, off by default.
Member
GET /tenants/{tenant_id}/members. PSA people for the time picker (ConnectWise System Members; Halo Agents). Public word is always member. Halo and ConnectWise list members when that permission is on. A missing ConnectWise members permission returns 502 upstream_auth_failure. Time write uses the listed id as CreateTimeEntryBody.member.
Device
GET /tenants/{tenant_id}/devices.
Agreement
GET /tenants/{tenant_id}/agreements. Recurring MSP service billing arrangement. The public word is Agreement.