Skip to main content

API quick-start

This guide sends your first message with a test key, then covers the rules every integration needs: authentication, idempotency, rate limits, errors and the sandbox.

1. Authenticate​

Send your key as a bearer token on every call.

curl https://api.bubbl3.com/v1/lines \
-H "Authorization: Bearer $BUBBL3_API_KEY"
KeyPrefixWhat it can do
Live keyb3_live_Sends to real contacts through your lines
Test keyb3_test_Sends only to test numbers, through the device simulator

A key has one or more scopes, chosen when it's created:

ScopeAllows
readReading contacts, conversations, messages, lines, outreach, workflows, agents and analytics
sendSending messages and replying in conversations
adminManaging developer settings, lines, contacts and workflows

admin doesn't include read; a key that needs both lists both. No key can reach billing, team, security or the audit log. A key can also be limited to a list of IP addresses.

2. Send a message​

curl https://api.bubbl3.com/v1/messages \
-H "Authorization: Bearer $BUBBL3_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-confirmation" \
-d '{ "to": "+13475550142", "body": "Your order is on its way." }'

The answer is 201 with the message, status: "queued". bubbl3 then picks a line, checks consent, quiet hours and line capacity, and sends it. Follow its progress with GET /v1/messages/:id, GET /v1/messages/:id/events, or the message.status webhook.

Use pool_id or line_id to choose where it's sent from, send_at to schedule it, fallback: false to forbid SMS fallback, and external_id and metadata to link it to your own records. See Messages for every field.

3. Retry safely with idempotency keys​

Network errors happen. Send an Idempotency-Key header on every write so a retry can't send twice.

  • The same key with the same request returns the first response, with idempotent-replayed: true.
  • The same key with a different request is 409 idempotency_conflict, as is a retry while the first request is still running.
  • Keys last 24 hours and belong to your workspace. They're 1 to 255 printable characters without spaces.
  • A 5xx answer frees the key, so retrying runs the request again. A 4xx answer is kept, so a retry gets the same answer.
  • Keys starting with outreach:, workflow: or agent: are reserved for messages bubbl3 sends itself and are refused with 400.

A good key names the thing you're doing once, such as order-1042-confirmation, not a random value made fresh on each retry.

4. Stay inside the rate limits​

CallsLimit per key
Sending (POST /v1/messages, replies, iMessage lookups)10 a second, bursts of 20
Everything else50 a second

Over the limit you get 429 rate_limited with a retry-after header in seconds. Wait that long, then retry with the same idempotency key.

Rate limits protect the API. Separately, every line has a daily and hourly limit on new contacts, and sends to people who haven't replied yet follow first-message rules. A message that would break those isn't refused; it's queued until it can go. GET /v1/lines shows each line's capacity left today.

5. Handle errors​

Every error has the same shape:

{
"error": {
"code": "validation_failed",
"message": "The request is invalid",
"request_id": "req_7Hc2LmQ9xT4bWz1R",
"details": {
"issues": [{ "location": "body", "path": "/to", "code": "invalid_format", "message": "Invalid phone number" }]
}
}
}

Branch on code, never on message. Quote request_id (also in the x-request-id header) when you contact support.

StatusCommon codesWhat to do
400validation_failed, bad_requestFix the request; don't retry it unchanged
401unauthorizedCheck the key
403forbidden, org_pausedThe key's scopes or IP allow list don't cover this call, or the organisation is paused
404not_foundThe id doesn't exist in this workspace
409conflict, idempotency_conflictRead the current state, or wait for the first request
413, 415payload_too_large, unsupported_media_typeSend a smaller JSON body
422contact_opted_outThe contact opted out; don't message them
429rate_limitedWait for retry-after
5xxinternal_error, service_unavailableRetry with backoff and the same idempotency key

6. Lists and pagination​

Lists take ?limit= (up to 100, default 50) and return { data, next_cursor }. Pass next_cursor back as ?cursor= until it's null.

7. Test with the sandbox​

With a test key:

  • You can only send to test numbers, +1 NPA 555 01xx (for example +13475550142). Anything else is 400 on /to.
  • Messages go to your workspace's sandbox line and the device simulator. They follow the whole pipeline (status changes, receipts and fallback), so your webhooks fire as they would in production.
  • Sandbox messages are marked is_test: true and never count towards line limits or health.
  • In the dashboard, Developers > Sandbox shows sandbox traffic and can send simulated replies.

When it works, swap in a live key. Nothing else changes.