API Reference · v1
Build on Sbl.so
Sbl.so runs AI-driven outreach — LinkedIn and WhatsApp campaigns, replies, and lead qualification. The REST API lets you manage campaigns and contacts programmatically; webhooks push every campaign event to your own systems in real time.
Overview
How the API is organized
Every request goes to a single base URL and is authenticated with a bearer token. Responses are JSON. Endpoints are scoped to a company — most requests take a companyId, and Sbl.so checks that the campaign or contact you’re asking for actually belongs to that company before returning anything.
| Property | Value |
|---|---|
Base URL | https://api.sbl.so |
Format | JSON request and response bodies |
Auth | Bearer token in the Authorization header |
Token lifetime | 30 days from issue |
Pagination | Cursor-based — pass the last item’s id back as lastXId |
Guide
Authentication
Every endpoint except token generation requires a bearer token. Fetch one with your account email and password, then attach it to every subsequent request.
Generate token
Authenticates a user with email and password and returns a bearer token valid for 30 days. If any other endpoint returns 401 Unauthorized, regenerate the token here and retry the request.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Required | Your account email address |
password | string | Required | Your account password |
Request example
curl https://api.sbl.so/auth/token \
-X POST \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "your_password"}'
Response — 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIsInR5..."
}
Errors
| Status | Description |
|---|---|
400 | Missing or invalid parameters |
401 | Invalid email or password |
429 | Too many requests — check the Retry-After header and retry |
Every authenticated request needs Authorization: Bearer <token> in its headers. Tokens are valid for 30 days — cache one and only re-request it when you hit a 401.
Guide
Errors & rate limits
Errors return a non-2xx status with details in the body. The status codes below recur across most endpoints; endpoint-specific cases are called out where they apply.
| Status | Meaning |
|---|---|
400 | Missing, invalid, or unknown fields in the request |
401 | Token expired or invalid — regenerate it via /auth/token |
403 | The campaign or company doesn’t belong to the authenticated user |
404 | Campaign or resource not found |
409 | A conflicting change — e.g. the record changed mid-update, or the user is already in the campaign |
429 | Too many requests — read the Retry-After header and back off before retrying |
Respect Retry-After. On a 429, the header tells you exactly how long to wait. Retrying immediately just extends the backoff.
Campaign endpoints
Campaigns
List, read, and update campaigns; manage the people inside them.
List campaigns
Returns a list of campaigns for a company.
Authentication
Authorization: Bearer <token>
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyId | string | Required | The ID of the company |
statuses | string | string[] | Optional | Filter by campaign status(es) |
types | string | string[] | Optional | Filter by campaign type(s) |
isArchived | boolean | Optional | Filter archived campaigns (default: false) |
searchQuery | string | Optional | Search campaigns by name |
lastCampaignId | string | Optional | Pagination cursor — ID of the last campaign from the previous page |
Response — 200 OK
[
{
"id": "123",
"name": "My Campaign",
"status": "running",
"type": "outbound",
"communicationChannelToUse": 1,
"statistics": {},
"createdAt": "2024-01-01T00:00:00.000Z"
}
]
Errors
| Status | Description |
|---|---|
401 | Token expired or invalid |
403 | Company does not belong to the logged-in user |
429 | Too many requests — check Retry-After header and retry |
Get campaign
Returns full details for a single campaign.
Authentication
Authorization: Bearer <token>
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
campaignId | string | Required | The ID of the campaign |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyId | string | Required | The ID of the company |
Response — 200 OK
{
"id": "123",
"name": "My Campaign",
"status": "running",
"type": "outbound",
"objective": "...",
"initialMessage": "Hello!",
"communicationChannelToUse": 1,
"statistics": {},
"analytics": {},
"batches": [],
"createdAt": "2024-01-01T00:00:00.000Z"
}
Errors
| Status | Description |
|---|---|
401 | Token expired or invalid |
403 | Company does not belong to the logged-in user |
429 | Too many requests — check Retry-After header and retry |
Update campaign
Updates a campaign. Send only the fields you want to change — everything else is left untouched, and unknown fields are rejected. Concurrency is handled for you: the API reads the campaign’s current revision before applying the update.
Authentication
Authorization: Bearer <token>
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
campaignId | string | Required | The ID of the campaign |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
companyId | string | Required | The ID of the company |
name | string | Optional | Campaign name |
objective | string | Optional | Campaign objective |
targetUsers | object | Optional | { title, description } — the ICP |
communicationChannelToUse | number | Optional | Required when updating initialMessage or followupMessages |
initialMessage | string | Optional | First-touch message, validated per channel |
initialMessageDelayInMs | number | Optional | Delay before the first message (max 90 days) |
followupMessages | array | Optional | [{ id, message, followupAfterInMs, asVoiceNote? }] |
smartFollowupsConfig | object | Optional | { delayInMs[], followupMessageReasoning[] } — equal-length arrays, min 5 minutes each |
nonConnectedLeadsMessage | string | Optional | Message for leads who aren’t connections yet |
sendInviteWithoutMessage | boolean | Optional | Send LinkedIn invites without a note |
productLinks | array | Optional | [{ link, description }] |
icpFilter | object | Optional | { criteria, requirements, networkDistances }. criteria and networkDistances are each optional but at least one is required; requirements is required alongside criteria. networkDistances: FIRST_DEGREE | SECOND_DEGREE | THIRD_DEGREE | OUT_OF_NETWORK |
userTagsToRunOn | array | Optional | Tag IDs to target |
leadListSetsToRunOn | array | Optional | Lead list set IDs to target |
campaignsToRunOn | array | Optional | Retargeting source campaigns |
commentToDM | array | Optional | [{ postUrl, trigger }] — LinkedIn post URLs, must be unique |
likeToDM | array | Optional | [{ postUrl }] — LinkedIn post URLs, must be unique |
pausedItem | number | Optional | Pause a specific part of the campaign |
isArchived | boolean | Optional | Archive or unarchive the campaign |
webhooksEnabled | boolean | Optional | Toggle webhooks for this campaign |
customAnalyticsFields | array | Optional | [{ name, description }] — unique, JSON-key-safe names |
Request example
{
"companyId": "456",
"name": "Q3 Founders Outreach",
"objective": "Book demos with seed-stage founders",
"communicationChannelToUse": 1,
"initialMessage": "Hi {{name}}, saw you're building in devtools..."
}
Response — 200 OK
{
"updated": true,
"status": 200,
"payload": {}
}
Errors
| Status | Description |
|---|---|
400 | Invalid or unknown fields in the body |
401 | Token expired or invalid |
403 | Company does not belong to the logged-in user |
404 | Campaign not found |
409 | Campaign changed while the update was being applied |
429 | Too many requests — check Retry-After header and retry |
List campaign users
Returns a paginated list of users in a campaign.
Authentication
Authorization: Bearer <token>
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
campaignId | string | Required | The ID of the campaign |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyId | string | Required | The ID of the company |
userChatStatus | number | Optional | Filter by user chat status |
searchQuery | string | Optional | Search users by name |
lastUserId | string | Optional | Pagination cursor — ID of the last user from the previous page |
Response — 200 OK
[
{
"id": "456",
"name": "John Doe",
"phoneNumber": "+1234567890",
"email": null,
"linkedinChannelDetails": null,
"tags": [],
"aiChatOff": false,
"status": 1,
"lastMessageCreatedAt": "2024-01-01T00:00:00.000Z",
"createdAt": "2024-01-01T00:00:00.000Z"
}
]
Errors
| Status | Description |
|---|---|
401 | Token expired or invalid |
403 | Company does not belong to the logged-in user |
429 | Too many requests — check Retry-After header and retry |
Get user conversation
Returns messages in a user’s conversation within a campaign, ordered oldest first.
Authentication
Authorization: Bearer <token>
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
campaignId | string | Required | The ID of the campaign |
userId | string | Required | The ID of the company user |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyId | string | Required | The ID of the company |
lastMessageId | string | Optional | Pagination cursor — ID of the last message from the previous page |
Response — 200 OK
[
{
"id": "789",
"senderType": "bot",
"content": "Hello, welcome!",
"type": "text",
"status": 2,
"communicationChannelUsed": 1,
"trigger": "initial",
"scheduledFor": null,
"createdAt": "2024-01-01T00:00:00.000Z"
}
]
Errors
| Status | Description |
|---|---|
401 | Token expired or invalid |
403 | Company does not belong to the logged-in user |
429 | Too many requests — check Retry-After header and retry |
List human intervention users
Returns users in the campaign that require human intervention.
Authentication
Authorization: Bearer <token>
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
campaignId | string | Required | The ID of the campaign |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
companyId | string | Required | The ID of the company |
lastUserId | string | Optional | Pagination cursor — ID of the last user from the previous page |
Response — 200 OK
[
{
"id": "456",
"name": "John Doe",
"phoneNumber": "+1234567890",
"email": null,
"linkedinChannelDetails": null,
"tags": [],
"aiChatOff": false,
"status": 1,
"lastMessageCreatedAt": "2024-01-01T00:00:00.000Z",
"createdAt": "2024-01-01T00:00:00.000Z"
}
]
Errors
| Status | Description |
|---|---|
401 | Token expired or invalid |
403 | Company does not belong to the logged-in user |
429 | Too many requests — check Retry-After header and retry |
Generate campaign
AI-generates a new campaign based on a natural language description.
Authentication
Authorization: Bearer <token>
Request body
| Name | Type | Required | Description |
|---|---|---|---|
companyId | string | Required | The ID of the company |
description | string | Required | Natural language description of the campaign goal |
communicationChannel | number | Required | 1 = WhatsApp, 3 = LinkedIn |
Request example
{
"companyId": "123",
"description": "Reach out to CTOs at fintech companies about our new product",
"communicationChannel": 1
}
Response — 200 OK
{
"id": "456",
"pendingCommentToDM": false,
"pendingMetaAdId": false,
"pendingProductLinks": []
}
Errors
| Status | Description |
|---|---|
400 | Missing or invalid parameters |
401 | Token expired or invalid |
403 | Company does not belong to the logged-in user |
429 | Too many requests — check Retry-After header and retry |
Send message in campaign
Sends a message to a specific user in a campaign.
Authentication
Authorization: Bearer <token>
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
campaignId | string | Required | The ID of the campaign |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
userId | string | Required | The ID of the company user |
message | string | Required | The message text to send |
Request example
{
"userId": "789",
"message": "Hello, following up on our conversation!"
}
Response — 200 OK
{
"messageId": "xxxx"
}
Errors
| Status | Description |
|---|---|
400 | User not in campaign, campaign not running, channel not connected, or user in an invalid chat state |
401 | Token expired or invalid — regenerate using /auth/token |
403 | Campaign does not belong to the logged-in user |
404 | Campaign not found |
429 | Too many requests — check Retry-After header and retry |
Add user to campaign
Adds a new user to a campaign. WhatsApp campaigns require phoneNumber; LinkedIn campaigns require linkedinProfileUrl. LinkedIn campaigns can also include customMessage for templates that use [custom message].
Authentication
Authorization: Bearer <token>
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
campaignId | string | Required | The ID of the campaign |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
name | string | Required | Full name of the user |
phoneNumber | string | Optional | Required for WhatsApp campaigns |
linkedinProfileUrl | string | Optional | Required for LinkedIn campaigns |
customVariables | object | Optional | Key-value pairs of custom data to attach to the user. Keys and values must be non-empty strings. |
customMessage | string | Optional | LinkedIn-only custom text used to replace [custom message] in campaign messages |
Request example
{
"name": "John Doe",
"linkedinProfileUrl": "https://www.linkedin.com/in/johndoe",
"customVariables": {
"company": "Acme Inc",
"role": "CTO"
},
"customMessage": "I noticed Acme recently expanded its engineering team."
}
Response — 200 OK
This endpoint returns an empty response body.
Errors
| Status | Description |
|---|---|
400 | Campaign not in a valid state, contact field doesn’t match the campaign channel, or customMessage used outside a LinkedIn campaign |
401 | Token expired or invalid — regenerate using /auth/token |
403 | Campaign does not belong to the logged-in user |
404 | Campaign not found |
409 | User is already part of this campaign |
429 | Too many requests — check Retry-After header and retry |
Webhooks
Overview & setup
Webhooks send real-time campaign updates to the tools your team already uses. Instead of polling the API, register an endpoint once and Sbl.so pushes an event to it the moment something happens — a connection accepted, a reply, a lead that needs a human.
Setting one up
From API Integration → Webhooks → Add webhook, in the app:
| Step | What you set |
|---|---|
1 | Endpoint URL — the HTTPS URL Sbl.so will POST each event to |
2 | Events to subscribe to — pick one or more of the seven event types below |
3 | Campaign filter — scope the webhook to all campaigns or a specific one |
Every event is delivered as an HTTP POST with a JSON body. Every payload shares the same envelope — event, timestamp, campaign, prospect, and sender — plus fields specific to that event, shown per event below.
Webhooks
Events & payloads
Seven event types cover the lifecycle of an outbound touch, from the first connection request to a lead that needs a human. Subscribe to only what you plan to act on.
connection_request_sentLinkedIn invite sentconnection_request_acceptedInvite acceptedmessage_sentOutbound message sentprospect_repliedProspect repliedmessage_failedOutbound message failedlead_action_neededFlagged for a humancustom_dataAI-extracted fieldsFires when a LinkedIn connection request is sent.
{
"event": "connection_request_sent",
"timestamp": "2026-06-10T00:00:00.000Z",
"campaign": {
"id": "483",
"name": "Outbound Campaign"
},
"prospect": {
"name": "Lokesh",
"linkedin_profile": "https://www.linkedin.com/in/example",
"phone_number": "+919****9999",
"email": "[email protected]"
},
"sender": {
"name": "Founder LinkedIn Profile"
}
}
Fires when the prospect accepts a LinkedIn connection request.
{
"event": "connection_request_accepted",
"timestamp": "2026-06-10T00:00:00.000Z",
"campaign": {
"id": "483",
"name": "Outbound Campaign"
},
"prospect": {
"name": "Lokesh",
"linkedin_profile": "https://www.linkedin.com/in/example",
"phone_number": "+919****9999",
"email": "[email protected]"
},
"sender": {
"name": "Founder LinkedIn Profile"
}
}
Includes the outbound message text, or "Attachment message" for media.
message{
"event": "message_sent",
"timestamp": "2026-06-10T00:00:00.000Z",
"campaign": {
"id": "483",
"name": "Outbound Campaign"
},
"prospect": {
"name": "Lokesh",
"linkedin_profile": "https://www.linkedin.com/in/example",
"phone_number": "+919****9999",
"email": "[email protected]"
},
"sender": {
"name": "Founder LinkedIn Profile"
},
"message": "Hi Lokesh, wanted to follow up on our conversation."
}
Includes the prospect’s reply text, or "Attachment message" for media.
message{
"event": "prospect_replied",
"timestamp": "2026-06-10T00:00:00.000Z",
"campaign": {
"id": "483",
"name": "Outbound Campaign"
},
"prospect": {
"name": "Lokesh",
"linkedin_profile": "https://www.linkedin.com/in/example",
"phone_number": "+919****9999",
"email": "[email protected]"
},
"sender": {
"name": "Founder LinkedIn Profile"
},
"message": "Thanks, can you share more details?"
}
Includes the failed outbound message text when available.
message{
"event": "message_failed",
"timestamp": "2026-06-10T00:00:00.000Z",
"campaign": {
"id": "483",
"name": "Outbound Campaign"
},
"prospect": {
"name": "Lokesh",
"linkedin_profile": "https://www.linkedin.com/in/example",
"phone_number": "+919****9999",
"email": "[email protected]"
},
"sender": {
"name": "Founder LinkedIn Profile"
},
"message": "Hi Lokesh, wanted to follow up on our conversation."
}
Fires when the lead is flagged for human intervention.
{
"event": "lead_action_needed",
"timestamp": "2026-06-10T00:00:00.000Z",
"campaign": {
"id": "483",
"name": "Outbound Campaign"
},
"prospect": {
"name": "Lokesh",
"linkedin_profile": "https://www.linkedin.com/in/example",
"phone_number": "+919****9999",
"email": "[email protected]"
},
"sender": {
"name": "Founder LinkedIn Profile"
}
}
Includes permanent prospect fields plus any custom fields extracted by AI.
customData{
"event": "custom_data",
"timestamp": "2026-06-10T00:00:00.000Z",
"campaign": {
"id": "483",
"name": "Outbound Campaign"
},
"prospect": {
"name": "Lokesh",
"linkedin_profile": "https://www.linkedin.com/in/example",
"phone_number": "+919****9999",
"email": "[email protected]"
},
"sender": {
"name": "Founder LinkedIn Profile"
},
"customData": {
"name": "Lokesh",
"linkedin_profile": "https://www.linkedin.com/in/example",
"email": "[email protected]",
"phone_number": "+919****9999",
"lead_score": 0.8,
"attachments": {
"images": true,
"audio_messages": true
}
}
}
Webhooks
Testing a webhook
Before wiring an event into production, verify your endpoint actually responds. The Webhook test panel on the Webhooks tab sends one sample event of your choosing straight to an endpoint URL — no webhook is created, and nothing gets saved until you’re ready.
Good to know: the sample payloads above are exactly what a ping test sends, so what you see in your endpoint logs during a test is what production traffic looks like.
Webhooks
Native integrations
Point-and-click destinations for teams who’d rather not stand up their own endpoint. Each one subscribes to the same events above and maps them into the target tool.
HubSpot
CRMSync contacts, deals, and sequences — every signal and reply flows straight into your HubSpot pipeline, no manual logging.
Slack
NotificationsGet a Slack ping the moment a high-intent buyer engages. React fast, route to the right rep, never miss a hot lead.
Zapier
AutomationConnect Sbl.so to 6,000+ apps via Zapier. Trigger workflows on signal events, new replies, or booked meetings — no code needed.
n8n
AutomationBuild advanced self-hosted automation flows with n8n. Full control over your data, logic, and integrations pipeline.
Notion
WorkspaceSend outreach activity and signal summaries to Notion databases through Zapier. Keep your whole team aligned in one place.
Salesforce
CRMPush intent signals and outreach activity into Salesforce objects in real time. Keep your reps working in the tool they know.
Pipedrive
CRMCreate deals and update stage automatically when a prospect replies or books. Your pipeline stays accurate without lifting a finger.
Make
AutomationDesign visual automation scenarios in Make. Route signals, enrich leads, and update any tool in your stack automatically.
Zoho CRM
CRMSend leads and campaign activity into Zoho CRM through Zoho Flow. Keep customer records current without manual updates.
Google Sheets
WorkspaceSend webhook events into Google Sheets through Make. Build a live, shareable record of campaign activity.
Custom server
WebhookSend events directly to your own webhook endpoint. Use your server to process, store, or route Sbl.so activity however you need.