One model underneath. Your product uses one API. The MSP connects their own tools.
01 — Your product integrates once
One schema, every vendor
Write against one model: tickets, companies, contacts, devices, and agreements. No per-vendor branches in your codebase.
02 — Customers connect themselves
Self-serve, guided, validated
The MSP pastes their own keys. You never store them. If a permission is missing, we name it in the flow so you do not take the support ticket.
03 — Act through the stack
Read and write, safely
Open a ticket, write back, close it with evidence. On tickets we opened.
Everything that comes with an integration
Every layer you'd otherwise build, bundled
Caching, retries, validation, and the vault ship with every connection.
auditevery read and write, visible
vaultMSP keys. Not yours.
previewsee a write before it lands
pacehonest, same on every vendor
storereads from here
Caching
Reads come from our store. They do not hit the PSA each time.
Rate-limit handling
We pace calls to each vendor’s real limits.
Retries
Transient failures are retried. You see one result.
Honest delivery status
No silent drop. A throttle is a real Retry-After.
De-duplication
A retry does not double-create a ticket.
Validation
Checked before they leave. Errors show in your request.
Credential vault
The MSP pastes keys. Your product never holds them.
Customer map
A sure match maps. Ambiguous stays unmatched.
Identity hold
Unmatched records can be held until someone releases them.
Health
Status and an audit log on every connection.
Developer experience
One call. Same shape every time.
one read — any PSA underneath
GET /tenants/{msp}/tickets
X-API-Key: blk_live_••••••••200 OK
{
"served_from": "store", // read from our synced store — zero vendor budget spent"last_synced_at": "2026-07-11T14:03:22Z",
"data": [{
"id": "tkt_8f31",
"status": "in_progress", // normalized across every PSA"summary": "VPN drops every few minutes",
"raw": { … } // the vendor's original payload, always there
}]
}
Writes are governedYou can preview a write before it lands.
.raw always rides alongThe vendor payload stays under .raw.
Same-day sandboxSeeded sandbox. First call the same day.
Also over MCP
The same model over MCP
REST and MCP use the same model.
Raw MCP servers
Agents pointed straight at vendor APIs
Every tool call is a live vendor request. It spends the shared rate budget.
Bitlync MCP
Sync-backed and governed
Reads come from the same store as the API. Writes use the same checks.
One normalized read
Same status. Every PSA.
normalized tickets — 3 sources, 1 shape
source
status
priority
summary
connectwise
waiting
critical
Server room UPS reporting battery fault
autotask
in progress
high
VPN drops every few minutes
halopsa
waiting
critical
Phishing report — possible compromise
Simulated data — three PSAs, one normalized read
The connect experience
MSPs connect themselves.
MSPs paste keys in a guided flow. We catch a missing permission before it becomes a ticket on your service desk.
Guided, in-flow
Guides sit beside the fields. Permissions stay in the flow.
Validated before it ships
Each key is tested. Bad setup is caught in the flow.
✓ authentication
✕ missing: Service Desk — Add
→ fix shown · re-test in place
✓ all 6 scopes verified
Hosted, credential-isolated
Keys stay on Bitlync. They do not touch your frontend.
Operated, not just hosted
See every connection. Read the log.
Health on the MSP. Every read and write in a log you can open.
A small number of paid design partners before general availability. Real integrations. A say in what gets built first.
How it starts
A conversation, not a form
A short conversation and an NDA.
What you get
Founding terms, locked
Founding terms locked in for the partners who join early.
What we ask
A real integration need
Candid feedback as we build, and a real gap we can solve together.
Overview
Start here.
Hold a key. Call a seeded tenant. Then the MSP pastes their own keys. There is no public sandbox URL on this page.
You sell software to MSPs (managed service providers). They run IT for other companies, on a stack they already chose. Winning those deals is much harder if you cannot connect to that stack.
PSA — their business system. Customers, tickets, service desk, projects, invoices, agreements.
RMM — their device system. Inventory, monitoring, patching, access.
Documentation — the detailed record. Passwords, networks, and what happened at that customer, linked to the PSA company and the RMM assets.
Commerce — where they buy licenses (direct or through a distributor) and then bill the customer.
Those tools do not share one API. Every MSP picked a different mix. Bitlync is one API with one shape. You write once. The MSP pastes their own keys. You do not hold them.
The MSP pastes keys. You never hold them. REST and MCP use the same model.
How it behaves — cache, rate limits, retries, vault, routing — lives in Concepts.
Connectors in the product today run against a seeded sandbox. Not a production AWS tenant.
Concepts
How the API behaves.
Store reads, pacing, writes, vault, and errors.
Caching / store-served reads
Reads come from the synced store for that MSP tenant. One sync fills the store. Every ISV on that tenant reads the same copy.
Mechanism
The caller asks for tickets, devices, or other listed reads. Bitlync answers from the store. That path does not spend vendor budget. live=True is an opt-in live read when a store read is not enough. There is no public live-read URL on this page.
What the caller sees
The response names the store and the last sync. Fields are served_from and last_synced_at.
No public live-read URL. No live-read host on this page.
Rate limits
We pace writes to the vendor’s real limit. If we have to wait, you get a rate-limit response (HTTP 429) with Retry-After. You do not guess a vendor quota.
One sync per MSP tenant fills the store. Every product on that tenant reads the same copy. When each product polls the same PSA, they share one budget.
Today — every product polls
With Bitlync — one governed sync
Mechanism
ISV-facing: 600 req/min times the number of connected MSPs. There is no daily cap. The per-minute rate is the limit. Hit it and you get a rate-limit response with Retry-After. There is no silent throttle.
What the caller sees
A rate-limit response you can branch on. The header is the wait. Do not treat a delayed 200 as a throttle.
Rate limit (429) + wait
HTTP 429
Retry-After: 12
{
"status": 429
}
No silent queue that looks like success. This page does not document a vendor daily cap.
Retries
Transient vendor rate limits (429) and 503 are retried with backoff on the Bitlync side of the write.
Mechanism
The caller sends one write. Bitlync retries the vendor when the failure is transient. The caller is not asked to implement vendor backoff for those hops.
What the caller sees
The caller sees one result. If the write still cannot land, the final rate-limit response (429) still carries the vendor Retry-After.
Final rate limit still honest
HTTP 429
Retry-After: 30
{
"status": 429
}
No hidden swallow of a vendor rate limit (429). No rewrite of a final rate limit into a 200.
Honest delivery
Delivery status is visible. Nothing is dropped silently.
Mechanism
A write that was acknowledged cannot double-write. A write that was throttled is a real rate-limit response (429), not a delayed success. Dedup and retries sit on this same rule.
What the caller sees
Success means the write landed or was an idempotent replay. Throttle means a rate-limit response (429) plus Retry-After.
acknowledged, not doubled
{
"idempotent_replay": false
}
No silent drop. No “accepted” that later vanishes.
Dedup
Writes that support it take idempotency_key. The same key does not open a second ticket.
Mechanism
Send the key on the write. A retry with that key is a replay, not a new vendor create.
Keys are not implied on every future write. Only writes that already support idempotency_key.
Validation
The payload is checked against that vendor’s rules before it leaves Bitlync.
Mechanism
A bad field fails on the request you sent. It does not create a ticket.
What the caller sees
HTTP 422. Branch on stable codevalidation.
422 on your request
HTTP 422
{
"status": 422,
"code": "validation"
}
No “send it anyway and let the PSA reject it.” No extra validation codes on this page.
Credential vault
The MSP pastes their own keys. Those keys sit in a per-tenant vault. The ISV never holds them.
Mechanism
ISVs start with a key that looks like blk_… and header X-API-Key. MSP credentials are entered on Bitlync, not in your frontend.
What the caller sees
Your product sends X-API-Key. It does not send the MSP’s PSA or RMM secret.
ISV auth header
X-API-Key: blk_…
# MSP keys are not in this header.
# They sit in the per-tenant vault.
The ISV does not paste MSP creds. There is no shared-secret path that puts PSA keys in your app.
Customer map
A sure match auto-maps. A leftover PSA company with no ISV candidate is an ask: the MSP creates or imports it in the ISV. Ambiguous UNMAPPED is a human match. It is never an auto-create.
Mechanism
Listed identity routes exist: GET /tenants/{tenant_id}/identity/mappings and GET /tenants/{tenant_id}/identity/unmapped.
What the caller sees
Mapped rows when the match is sure. UNMAPPED when a person has to choose. Creating a leftover PSA company in the ISV returns 501 (OPEN).
Creating a leftover PSA company in the ISV returns 501. No silent create.
Identity hold
Unmatched records can sit in quarantine until someone releases them.
Mechanism
Hold is a reserved record, not an auto-merge. The reserved shape name is QuarantinedRecord.
What the caller sees
Unmatched work stays unmatched until release. Do not treat this page as a schema for that record.
reserved name only
{
"status": "UNMAPPED"
}
# Reserved shape: QuarantinedRecord
# Fields are not documented on this page.
Health + incidents + audit
Status and an audit log sit on every connection. After the MSP connects, a role connector attaches so the ISV can see that MSP on health and status. Failed auth attaches nothing. Vendors with no role audit unknown, not success. Incidents are a listed read. Not a new connect UI.
GET /incidents can name which ISV, MSP, and field sit on a moved vendor field. Sandbox seeds one open Autotask incident with that list. That is not the same as one MSP connection going degraded. We do not patch the vendor.
Mechanism
GET /tenants/{tenant_id}/audit exists. Also listed: /status, /incidents, /connections, and /connections/events.
What the caller sees
Audit is a GET on the tenant.
GET audit
curl -s "$BITLYNC_BASE/tenants/sandbox-mainstreet/audit" \
-H "X-API-Key: $BITLYNC_KEY"
GET /tenants/{tenant_id}/audit
GET /tenants/{tenant_id}/status
GET /tenants/{tenant_id}/incidents
No public ops host on this page.
Raw
The canonical Bitlync shape is the default. The vendor payload always rides on raw.
Mechanism
Normalize status, source, and the other Bitlync words. Keep the vendor’s original object under raw. Never drop it.
What the caller sees
You can stay on the canonical fields. You can read raw when a vendor word is not in the model.
No mode that strips raw. No second public schema that replaces the canonical default.
Writes
dry_run defaults true. That is a preview with no side effect. Commit by sending dry_run false. After Bitlync creates a ticket, you can update it, append a note, close with evidence, or POST hours on that ticket only. Close stays off unless the MSP turns it on. Hours take stated billable and an optional member. No notes on time. Not a time read. Not two-way. Not field-level ticket sync.
Mechanism
Preview first. Then commit with the same body and an idempotency_key on writes that support it. If a fan-out is only partial, that failure is explicit. It is never collapsed into a single silent success.
What the caller sees
A dry run does not create a ticket. A commit does. Partial failure is visible on the response, not hidden.
Ticket create, write-back, and time run on fixtures and dry_run for ConnectWise, Halo, and Autotask. dry_run defaults true. Halo pulls agreements and line items, the same objects as ConnectWise and Autotask. ConnectWise, Autotask, and Halo write line items on an existing agreement: add, rewrite, end, replace. ConnectWise, Autotask, and Halo pull the project container and write on an existing project. We do not mint a project header. Baseline cannot take that write. Creating a leftover PSA company in the ISV returns 501. Pax8 matches a company it already has, then creates the rest.
The MSP can approve or dismiss a mapped line item. Approve writes the line on an existing agreement. Dismiss does not change the agreement. ConnectWise only.
Seeded fixtures only: sandbox-mainstreet and sandbox-northline. Use $BITLYNC_BASE as the host placeholder. There is no public sandbox host on this page.
Mechanism
Every sandbox response includes sandbox true. POST /sandbox/reset with that tenant id clears audit, re-syncs fixtures, and writes one sandbox_reset event. A production tenant on that route returns 422 request_invalid.
Seed also includes one open Autotask incident with a fixture affected list. Northline Halo degraded is a separate connection status.
No public sandbox URL. No production host printed here. Connectors on this site run against a seeded sandbox, not a production AWS tenant.
Errors
Branch on the stable code. Do not parse a human sentence.
Mechanism
The codes on this page are the codes that exist: 401 auth, 403 tenant, 404 unknown tenant, 422 validation (and request_invalid on a bad sandbox reset), 429 plus Retry-After, 501 unsupported / parked capability, 500 opaque internal_error.
No extra codes on this page. 500 stays opaque. A parked write is 501, not a generic 500.
MSP permission / probe
A missing write permission can still connect as partial. Reads work. The parked write is refused distinctly. That is not a generic 500.
Mechanism
The MSP pastes keys in the guided flow. The probe names the permission to fix. The ISV does not paste MSP creds.
What the caller sees
Reads still serve from the store. A parked write is 501 unsupported. Portal copy tells the MSP which permission to fix.
parked write, not 500
HTTP 501
{
"status": 501,
"code": "unsupported"
}
# Portal copy names the permission.
# ISV does not paste MSP creds.
No ISV-side form that collects PSA keys. Partial connect is not collapsed into “failed.”
Routing
A finding lands as a PSA ticket until the MSP chooses otherwise. The ISV does not force workflows.
Mechanism
Listed routes: POST /tenants/{tenant_id}/findings/route and GET / PUT /tenants/{tenant_id}/routing. Alert to ticket is the path. Incident and approval sit on that same path.
What the caller sees
The finding becomes a ticket in the PSA the MSP already runs. Routing changes are the MSP’s.
finding to ticket
POST /tenants/{tenant_id}/findings/route
GET /tenants/{tenant_id}/routing
PUT /tenants/{tenant_id}/routing
{
"source": "psa",
"raw": {}
}
Quickstart
From key to first write.
Seeded tenants are fixture-backed only. Use $BITLYNC_BASE as the host placeholder.
Keys look like blk_…. Send them as X-API-Key.
auth
X-API-Key: blk_…
tenant_id: sandbox-mainstreet
# or sandbox-northline
sandbox-mainstreet and sandbox-northline are sandbox fixtures (PSA and RMM seed data). They are not production tenants.
POST /sandbox/reset with {"tenant_id":"sandbox-mainstreet"}. Clears audit, re-syncs fixtures, writes one sandbox_reset event. Production tenants return 422 request_invalid.
Same CanonicalService as REST. Four tools: list_tickets, list_devices, list_installed_applications, route_finding. route_finding is the write. There is no public MCP host. Use $BITLYNC_BASE.
Use the Bitlync MCP. Tenant sandbox-mainstreet. Send X-API-Key.
Read with list_tickets, list_devices, or list_installed_applications.
The write tool is route_finding. Do not create tickets over MCP.
dry_run ticket create is REST-only.
Reference
Routes that exist today.
Auth is the X-API-Key header. Writes take dry_run (default true). Import the OpenAPI file. This page is not the spec.
List and create. Write back (update, note, close-with-evidence, hours) only on tickets Bitlync created. Close defaults off. Hours are a write, not a read. dry_run defaults true.
Device inventory lives here. It is not a use case.
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
POST
/tenants/{tenant_id}/findings/route
POST
/sandbox/reset
Sandbox tenants only
There is no GET /agreements.
Ticket
GET and POST /tenants/{tenant_id}/tickets. Write-back and hours only on tickets Bitlync created. Time has no notes field.
Company
POST /tenants/{tenant_id}/companies. Pax8 matches a company it already has, then creates the rest. We do not mint a PSA company. Creating a leftover PSA company in the ISV still returns 501.
Device
GET /tenants/{tenant_id}/devices. Inventory is not a use case. The route lives here.
Agreement
Recurring MSP service billing arrangement. Not “PSA Contract.” There is no GET /agreements.
What we write
What we write.
Line items on an existing agreement. Locks and refusals.
ConnectWise, Autotask, and Halo write line items on an existing agreement. Verbs: add, rewrite, end, replace. dry_run defaults true and returns the same payload. Apply is fixtures and dry_run. A host that is not a fixture returns 501. We do not mint an agreement header.
You need an active agreement so we can write the line.
We never mint an agreement header. We never push a company into the PSA. We never two-way tickets or watch the MSP close. We never write devices. We never auto-create an unmatched company.
If they try anyway: a stable code, one English sentence, and doc_url.
agreement_header_not_supported — they tried to mint a header. Autotask’s API would create one. Bitlync still refuses. That is policy.
agreement_requires_existing — missing or unknown agreement.
ConnectWise, Autotask, and Halo write on an existing project. We do not mint a Project header. Baseline cannot take that write.
project_requires_existing — missing or unknown project.
project_header_not_supported — they tried to mint a Project header. Bitlync still refuses. That is policy.
project_not_writable — Baseline cannot take that write.
project_company_cannot_update — the project’s company cannot be updated.
Pax8 matches companies it already has, then creates the rest. We do not mint a PSA company.
psa_company_create_refused — they aimed create at the PSA. Autotask’s API would create one. Bitlync still refuses. That is policy.
company_match_ambiguous — more than one match. We do not create.
distributor_required — no connected distributor for this write.
Use cases
The paths.
Alert to ticket, customer map, time on a linked ticket, close with evidence. Device inventory is not a use case.
1. Alert to ticket
A finding becomes a ticket we created. We keep the link. You can update, append a note, or close with evidence on that ticket only. Close stays off unless the MSP turns it on. Not two-way. We do not watch the MSP close or edit. Incident and approval sit on this path.
2. Map customers
Sure match auto-maps. A leftover PSA company is an ask. Ambiguous stays unmatched. Creating it in the ISV returns 501.
3. Time on the ticket
POST hours on a ticket Bitlync already linked. Hours, stated billable, optional member. Same write grant. Fixtures and dry_run. Not a time read. No list. No timers. No notes field. Not two-way time. No billing codes, work types, or start–end.
4. Close with evidence
On tickets Bitlync created: append evidence, or close if the MSP turned that on. Close defaults off. Fixtures and dry_run. Not two-way ticketing.
Libraries
curl, Python, TypeScript.
curl plus bitlync_client in clients/python (not published) and clients/typescript (not published). Use $BITLYNC_BASE only.