Alerts
Alerts.
List the alerts you send. Each MSP chooses where they land in their stack. Bitlync routes them. You do not pick a board, a team, or a form for them.
There is no breaking change to POST /tenants/{tenant_id}/alerts. The Activity tab does not add an API. Billing and Alerts write routes share one abuse limit. HTTP 429 is unchanged. See Rate limits.
Setup
Turn on Send alerts to MSP tickets in Configure > Features. Setup offers Upload a list (CSV or Excel) or Add by hand, then a review before the list is live.
Add by hand, and any later edit, is PUT /isv/alert-types (the same body as PUT /tenants/{tenant_id}/alert-types). That call replaces the whole catalog. It does not merge. A type you leave out is removed. Category is required. Severity is Critical, High, Medium, Low, or Info. The type menu is Rename, Edit, Remove, and Duplicate. In the console, click a type name or description to edit it in place. Enter or leaving the cell saves. Escape cancels. The row shows Saving until the save finishes. Removing the last type returns to this guided setup.
Upload a list on the API is POST /isv/alert-types/import. The body is {"csv": "..."}. Columns are name, description, type, and severity. Nothing is saved. A CSV with no name column returns 422 request_invalid and "The CSV needs a name column." A one-row sandbox call returned count 1, draft true, and one object in alert_types: id, name, description, category Product alerts, group product, suggested_severity, tags (the type column), and draft true. The catalog GET after that call still had 11 rows.
An Alerts or Billing upload is at most 2 MB. An alert write that is refused returns "Reload the page and try again."
Catalog
GET /isv/alert-types and GET /tenants/{tenant_id}/alert-types return AlertTypeList. Omit limit and the whole catalog comes back, at most 200 types. limit is 1 to 200. Pass next_cursor back as cursor. next_cursor is null on the last page. count and total are the catalog size.
Each row is AlertTypeOut. Required fields are id, name, category, and suggested_severity (strings). The schema also has description, group, group_label, fix_line, and suggested_severity_label (strings), tags (array of strings), notify_days (integer or null), and draft (boolean). The list wrapper requires alert_types, count, total, and sandbox. Sync failing comes back with suggested_severity by_cause. The PUT severity check is Critical, High, Medium, Low, or Info.
{
"id": "integration-changes",
"name": "Integration changes",
"description": "Tells MSPs when you add, remove or change a service.",
"category": "Integration health",
"group": "integration",
"group_label": "Integration health",
"tags": ["Integration"],
"notify_days": null,
"fix_line": "",
"draft": false,
"suggested_severity": "medium",
"suggested_severity_label": "Medium"
}
That object is the Integration changes row from the sandbox catalog. The full GET returned 11 rows, count 11, total 11, and next_cursor null. A limit of 1 returned the Subscription renewal row, the same count and total, and a next_cursor string.
PUT /isv/alert-types and PUT /tenants/{tenant_id}/alert-types take AlertTypesBody. alert_types is an array of AlertTypeWrite. Required on each row: name and category. Optional: suggested_severity or its alias severity, description, id, group, notify_days, fix_line, and draft. A value that is not a list returns 422 request_invalid and "alert_types must be a list." Severity must be Critical, High, Medium, Low, or Info. Anything else returns 422 request_invalid and "Severity must be Critical, High, Medium, Low or Info." At most 200 types. The body is at most 512 KB. A missing length is 411. An oversized body is 413. The 200 body is AlertTypeWriteResult: alert_types (AlertTypeOut) and count.
Groups
The heading is Categories. Types sit in three groups. Subscriptions & billing starts with six types. Integration health starts with five. Product alerts starts empty. These are the starter rows GET /isv/alert-types returned on the sandbox.
| Category | Starter name |
|---|---|
| Subscriptions & billing | Subscription renewal |
| Subscriptions & billing | Price change |
| Subscriptions & billing | Quantity updated |
| Subscriptions & billing | Trial ending |
| Subscriptions & billing | Trial converted |
| Subscriptions & billing | Trial expired |
| Integration health | Connection problem |
| Integration health | Sync failing |
| Integration health | Mapping needs attention |
| Integration health | Stack slow or rate limited |
| Integration health | Integration changes |
Product alerts has no starter types. An upload is reviewed there before it is live.
MSP connect
Connect adds an alerts setup step with Back and Continue. The MSP chooses where alerts go, using fields from their stack. Type is required. Subtype (ConnectWise), Item, and the Halo subcategory are optional and default to None. The error "ConnectWise requires Subtype on {board}." is returned only when the stack mandates Subtype. The sandbox stack options for the fixture ConnectWise tenant list Board, Type, Subtype, and Item. One Item name in that fixture is None. The sandbox did not return the Subtype error string. No board was saved on that call.
If the save cannot be read back, the message is "Reload the page and try again." If saves come too fast, the message is "Too many saves. Wait a minute and try again."
The Done page links "Go to Activity". The sandbox Connect preview that was fetched was still on the keys step, so that link was not in the HTML. The step rail was Choose, Connect, Configure, Map companies, Done.
Activity
The MSP portal is not available. Portal routes, including /portal/login/request and /portal/login/complete, return 404 portal_unavailable with the message "The MSP portal is not available." No sign-in email is sent. The console Activity destination returned 401 for the sandbox demo key, so it was not opened.
A test ticket title looks like [Bitlync test] {vendor display name}: .... On a sandbox dry run, priority_name was Informational, and dry_run true did not create a ticket. Repeat alerts for the same device update one open ticket when dedupe is on. Bitlync does not reopen a ticket the MSP closed. That dedupe rule is already in the alert send description.
Where an alert goes
Alert routing is set by the MSP in Connect. PUT /tenants/{tenant_id}/alert-routing 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, that PUT returns 409 routing_msp_owned and the stored row is unchanged. The message is "The MSP sets alert routing for this tenant in Connect. Nothing was saved." Nothing is sent on that response. Send the alert with POST /tenants/{tenant_id}/alerts.
GET /tenants/{tenant_id}/alert-routing returns AlertRoutingView. Optional query vendor_key defaults to connectwise. Required fields are vendor_key, rule (object of string arrays), priority_map (object of strings), coming_soon (AlertComingSoon: id and label), dedupe_line, alert_types (AlertRoutingType), count, and sandbox.
On the sandbox ConnectWise tenant, rule maps critical and high to ["ticket"], medium to ["email"], and low and info to ["daily_digest"]. coming_soon is teams (Teams or Slack) and webhook (Webhook). An AlertRoutingType requires id, name, severity, send_to, send_label, custom, off_allowed, and by_severity. group, note, and notify_days are on the schema too.
{
"vendor_key": "connectwise",
"rule": {
"critical": ["ticket"],
"high": ["ticket"],
"medium": ["email"],
"low": ["daily_digest"],
"info": ["daily_digest"]
},
"coming_soon": [
{"id": "teams", "label": "Teams or Slack"},
{"id": "webhook", "label": "Webhook"}
],
"count": 11,
"sandbox": true
}
The PUT body is AlertRoutingBody: optional vendor_key, send_to (object whose values are a string, an array of strings, or null), reset (array of strings), and dedupe (boolean). The description says Send to must always include Ticket or Email. The GET rule uses daily_digest for Low and Info. The 200 body is AlertRoutingView.
Severity bands: Critical and High share the High band, Medium is its own band, and Low and Info share the Low band. By default the High band goes to Ticket, Medium to Email, and Low to Daily digest. The MSP can set its own severity per alert type in Connect. Routing then uses the band for the MSP severity, not the catalog severity. An explicit Send to on the row still wins. Your catalog suggested_severity does not change.
Digests are not sent in production today. The daily digest and the monthly summary are both off there. An alert whose row goes to Daily digest or Monthly digest is delivered at send time instead: to the ticket when that row also has Ticket, otherwise as an email to the MSP's digest recipients. For the email case, POST /tenants/{tenant_id}/alerts returns action "emailed" and reason "digest_realtime", with an empty ticket_ref.
Where digests are on, a Monthly digest row queues the alert for the monthly summary, sent once per month on the MSP's chosen day and hour. The summary headline reads "{X} alerts across {Y} customers. {Z} still open." The counts include each customer row, including those behind +N more, and leave pricing out. A summary with only pricing changes leads with the change count.
Send
POST /tenants/{tenant_id}/alerts sends one alert. The body is AlertSendBody. type or alert_type is the name or id. severity is optional and defaults to the type's suggested severity. device is a string or AlertDevice (id and name, both optional strings). customer or company_name names the customer. description, event_id, and idempotency_key are optional strings. Omitted dry_run is false. The body is at most 64 KB. A missing length is 411. An oversized body is 413.
Send event_id or idempotency_key on every call. With dedupe on, the first create for a device still commits when both are omitted, because the dedupe key blocks a second identical create. A note, a create with dedupe off, or a repeat create with both ids omitted returns 422 request_invalid and "idempotency_key or event_id is required on commit (dry_run=false)". A call with a type and neither a customer nor a device returned 422 request_invalid and "Send a type, a customer, and a device." Alerts off returns 409 feature_off before a vendor call.
{
"type": "subscription-renewal",
"severity": "low",
"device": {"id": "dev-1", "name": "FRONT-DESK"},
"customer": "Acme Dental",
"event_id": "docs-probe-dry-2026-10-21",
"dry_run": true
}
The 200 body is AlertSendResult. Required: sandbox, action, reason, and alert_type (AlertTypeOut). Also on the schema: success, source, source_id, idempotent_replay, dry_run, message, raw, reopen, ticket_ref, title, severity, destination, priority_id, priority_name, dedupe, and snoozed. On the sandbox first-ticket call, the title was [CrowdStrike] Low · Subscriptions & billing · Subscription renewal · Acme Dental, reopen was false, and priority_name was an empty string.
The send result does not echo the ticket that went to your stack. The connector's raw, summary, and priority fields are dropped, because that ticket can carry the MSP's own severity. title and severity are your values.
Test
POST /tenants/{tenant_id}/alerts/test sends a test through the same routing. The body is AlertTestBody or null. It has the same fields as AlertSendBody. Omitted dry_run is true. The body is at most 64 KB. A missing length is 411. An oversized body is 413. The 200 body is AlertSendResult.
When the body does not name a type, the call uses the first alert type in the catalog. On the sandbox, {} used subscription-renewal (Subscription renewal, suggested severity low). title was [Bitlync test] CrowdStrike: no action required, safe to close, severity was low, priority_name was Informational, priority_id was 8, and dry_run was true. The spec uses sample type test-alert when the catalog is empty. This catalog was not empty.
Snooze
GET /tenants/{tenant_id}/alert-snoozes returns SnoozeList. At most 500 are stored. limit is 1 to 100. Omitted, the page size is 25. Required: snoozes (array of Snooze), count, and sandbox. next_cursor is a string or null. An empty list is count 0 and snoozes [].
A Snooze requires id, tenant_id, isv_key, scope, target_ref, duration, and created_at. Also on the schema: target_label, alert_type_id, and ends_at (string or null).
POST /tenants/{tenant_id}/alert-snoozes takes SnoozeCreateBody. The schema requires scope and duration, and marks target and target_ref optional. A target is required anyway: the customer or the device, in target or target_ref. That optional marking is a known schema bug. A body with only scope and duration returns 422 request_invalid and "Name the customer or device." Scope must be customer or device. Duration is 24h, 7d, or until_resolved. alert_type_id is optional. The body is at most 64 KB. The 200 body is SnoozeCreated: snooze and sandbox. until_resolved comes back with ends_at null.
{
"scope": "customer",
"target": "docs-probe-cust",
"duration": "24h"
}
{
"snooze": {
"id": "snz_8fdc48b1a10b70c9",
"tenant_id": "sandbox-mainstreet",
"isv_key": "demo-isv",
"scope": "customer",
"target_ref": "docs-probe-cust",
"target_label": "docs-probe-cust",
"alert_type_id": "",
"duration": "24h",
"ends_at": "2026-10-22T21:52:13.706524+00:00",
"created_at": "2026-10-21T21:52:13.706524+00:00"
},
"sandbox": true
}
POST /tenants/{tenant_id}/alert-snoozes/{snooze_id}/undo returns SnoozeRemoved: removed (the id string) and sandbox. The route description says a snooze on another MSP is 404.
Send to
MSP Send to is a flat checkbox list. Each section has one "Tickets go to" footer, with a Send test ticket link. The choice autosaves, and a quiet check mark shows when it has saved. Subscriptions tickets stay Off until the MSP picks a destination. The MSP can close the test ticket from Bitlync.
The footer is "Powered by bitlync", with a lowercase b.