bitlync Request early access

One API for ISVs

Write once. Connect to many.

Ship the product. Don’t own the integration stack.

PSA RMM Commerce Productivity Documentation GRC/vCIO Quoting/CPQ bitlync Your product

The product

How Bitlync works

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
sourcestatusprioritysummary
connectwisewaitingcriticalServer room UPS reporting battery fault
autotaskin progresshighVPN drops every few minutes
halopsawaitingcriticalPhishing 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.

your MSP connections142 connected
Blue Spruce ITConnectWise · synced 3 min ago
healthy audit log →
Harbor Managed ServicesHaloPSA · synced 6 min ago
healthy audit log →
Cedar Ridge TechnologyAutotask · retrying
degraded audit log →

Mockup. Simulated data.

See the roadmap. Thumb an issue to vote.

For MSPs

Saw “Secured by Bitlync”
in a connect screen?

One of your vendors uses Bitlync to connect to your stack. Here is what that means, and how you stay in control.

Grants, not shared credentials

You own the connection. Vendors get grants.

You connect once. Each vendor gets a scoped grant. Keys stay in your vault. Revoke a grant and that vendor is gone.

What a vendor can touch

You see the scope before you connect

Every grant is shown in plain language, including what they can never do:

Readservice tickets, companies, configurations
Writecreate tickets on a board you approve
Neverdelete records · read financials · change your configuration

Checked in the flow

We test the keys in the flow

If a permission is missing, we name it. You fix it and re-test.

✓ keys work
✕ missing: Service Desk — Add
→ fix shown · re-test here
✓ keys tested

Our data posture

Plainly stated

Synced once per tenant

One sync serves every vendor you granted.

Encrypted per-tenant

Per-tenant keys. Isolated from every other tenant.

Owned by you

Revoke a grant in one click. Delete your data on request.

Audited, append-only

Every read and write lands in a log you can see. It cannot be edited.

Support that can't touch your data

Support is read-only. It can diagnose. It cannot change your setup.

Never resold

Your operational data is not sold. Not aggregated.

SOC 2 in progress.

Your portal

See it. Scope it. Revoke it.

A permissions page. Not another app to manage.

your connections
CW
ConnectWise PSA1 vendor granted · read + scoped write
healthy · synced 4 min ago
NJ
NinjaOne RMM1 vendor granted · read only
healthy · synced 11 min ago

Mockup. Simulated data.

Early access

Build this with us.

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.

Your product bitlync one shape PSA RMM Documentation Commerce

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.

Quickstart Reference

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.

store-served read
{
  "served_from": "store",
  "last_synced_at": "2026-07-11T14:03:22Z",
  "data": [{
    "status": "in_progress",
    "source": "psa",
    "raw": {}
  }]
}

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

ISV A ISV B ISV C ISV D ISV E PSA API shared budget Five products. Same reads. One noisy poll starves the MSP.

With Bitlync — one governed sync

ISV A ISV B ISV C ISV D ISV E bitlync store PSA one sync Store-served reads. Zero vendor calls. N products. Same one 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.

What the caller sees

The replay is marked idempotent_replay.

same key
{
  "summary": "Printer offline",
  "dry_run": false,
  "idempotency_key": "ticket-1"
}

{
  "status": "ok",
  "idempotent_replay": true
}

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 code validation.

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).

unmapped is human
{
  "status": "UNMAPPED"
}

HTTP 501
{
  "status": 501,
  "code": "unsupported"
}

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.

raw always present
{
  "status": "in_progress",
  "source": "psa",
  "raw": {}
}

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.

dry_run then commit
{
  "summary": "Printer offline",
  "dry_run": true,
  "idempotency_key": "ticket-1"
}

{
  "summary": "Printer offline",
  "dry_run": false,
  "idempotency_key": "ticket-1"
}

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.

What we write lists the locks and refusals.

Sandbox

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.

What the caller sees

sandbox flag + reset
{
  "sandbox": true,
  "served_from": "store"
}

POST /sandbox/reset
{ "tenant_id": "sandbox-mainstreet" }

HTTP 422
{
  "status": 422,
  "code": "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.

What the caller sees

stable code
HTTP 401  { "status": 401, "code": "auth" }
HTTP 403  { "status": 403, "code": "tenant" }
HTTP 404  { "status": 404 }
HTTP 422  { "status": 422, "code": "validation" }
HTTP 429  Retry-After: 12
HTTP 501  { "status": 501, "code": "unsupported" }
HTTP 500  { "status": 500, "code": "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.

GET tickets

GET tickets
curl -s "$BITLYNC_BASE/tenants/sandbox-mainstreet/tickets" \
  -H "X-API-Key: $BITLYNC_KEY"

POST tickets

dry_run defaults true. Commit by sending dry_run: false plus an idempotency_key. The same key on a retry returns idempotent_replay.

POST /tenants/{tenant_id}/tickets

Header X-API-Key: blk_….

body · dry_run then commit

{
"summary": "Printer offline",
"dry_run": true,
"idempotency_key": "ticket-1"
}
then commit
{ "dry_run": false, "idempotency_key": "ticket-1" }

GET audit

GET audit
curl -s "$BITLYNC_BASE/tenants/sandbox-mainstreet/audit" \
  -H "X-API-Key: $BITLYNC_KEY"

Sandbox reset

POST /sandbox/reset with {"tenant_id":"sandbox-mainstreet"}. Clears audit, re-syncs fixtures, writes one sandbox_reset event. Production tenants return 422 request_invalid.

POST /sandbox/reset
curl -s -X POST "$BITLYNC_BASE/sandbox/reset" \
  -H "X-API-Key: $BITLYNC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tenant_id":"sandbox-mainstreet"}'

MCP

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.

MCP config
{
  "mcpServers": {
    "bitlync": {
      "url": "$BITLYNC_BASE",
      "headers": {
        "X-API-Key": "$BITLYNC_KEY"
      }
    }
  }
}
agent prompt
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.

OpenAPI 0.54.0

GET /tenants/{tenant_id}/tickets

POST /tenants/{tenant_id}/tickets

Tickets

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.

curl · JSON

snippet
curl -s "$BITLYNC_BASE/tenants/sandbox-mainstreet/tickets" \
  -H "X-API-Key: $BITLYNC_KEY"
MethodPathNotes
GET/tenants/{tenant_id}/devicesDevice 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/resetSandbox 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.

curl

curl
curl -s "$BITLYNC_BASE/tenants/sandbox-mainstreet/tickets" \
  -H "X-API-Key: $BITLYNC_KEY"

Python

Package name bitlync_client in clients/python. In-repo. Not published.

bitlync_clientclients/python · not published
from bitlync_client import Client

client = Client(base="$BITLYNC_BASE", api_key="blk_…")
tickets = client.tickets.list(tenant_id="sandbox-mainstreet")

TypeScript

In-repo at clients/typescript. Not published.

TypeScriptclients/typescript · not published
import { Client } from "./clients/typescript"

const client = new Client({ apiKey: "blk_…" })
const tickets = await client.tickets.list("sandbox-mainstreet")