Errors
Failed requests return a standard HTTP status code and an RFC 9457 problem details body with Content-Type: application/problem+json. Branch on the code field, not the human-readable text.
{
"type": "https://www.bloomtext.com/developers/api/errors/#insufficient_scope",
"title": "Forbidden",
"status": 403,
"code": "insufficient_scope",
"detail": "This endpoint needs the messages.write scope, and the user didn't grant it.",
"request_id": "0b6c2d1e-5f4a-4c3b-9a8d-7e6f5a4b3c2d",
"field_errors": []
}The problem object
typeURLRequiredLink to the documentation for this error.
titlestringRequiredShort, human-readable summary of the status.
statusintegerRequiredThe HTTP status code, repeated for convenience.
codestringRequiredMachine-readable error code. See Error codes.
detailstringExplanation of this specific occurrence.
request_idUUIDRequiredUnique ID for the request. Include it when you contact support.
field_errorsarray of FieldErrorRequiredPer-field problems for 422 responses. Empty otherwise.
Show child attributes
pathstringRequiredJSON pointer to the field, like /body, or the query parameter or header name.
codestringRequiredMachine-readable validation code, like too_long or required.
messagestringRequiredHuman-readable explanation.
HTTP status codes
| Status | Meaning | Retry? |
|---|---|---|
200, 201, 204 | Success. | No |
400 | The request is malformed. | No, fix the request. |
401 | The API key or access token is missing, expired, or revoked. | After refreshing an OAuth token. |
403 | The credential lacks the scope, or an API key called an OAuth-only endpoint. | No |
404 | The resource doesn’t exist, or it’s a conversation the connected admin isn’t in. | No |
409 | An idempotency or state conflict. | Sometimes. See the code. |
422 | A field failed validation. | No, fix the fields. |
429 | Rate limited. | Yes, after Retry-After seconds. |
500, 503 | A temporary problem on our side. | Yes, with backoff and the same Idempotency-Key. |
Error codes
| Code | Status | What to do |
|---|---|---|
invalid_request | 400 | Check the JSON syntax and query string. |
range_required | 400 | With an API key, List messages needs after, patient_id, or conversation_id. See Read messages. |
patient_filter_required | 400 | Find patients needs phone, mrn, or both. See Patients. |
invalid_query | 400 | The q search query can’t be parsed. detail points at the problem. See Search. |
invalid_token | 401 | API key: check it hasn’t been deleted. OAuth: refresh the access token. If the refresh fails with invalid_grant, the admin must reconnect your app. See When a connection ends. |
insufficient_scope | 403 | The credential lacks the scope listed on the endpoint’s reference page. Create a key with that scope, or send the admin through the authorize URL again. |
oauth_required | 403 | API keys can’t call this endpoint. Everything under /me, and the staff directory, requires an OAuth token. |
not_found | 404 | Check the ID. Conversations the connected admin isn’t in also return this. |
idempotency_key_reused | 409 | Use a new Idempotency-Key for a different request. |
idempotency_request_in_progress | 409 | The original request is still running. Retry shortly. |
ambiguous_recipient | 409 | The phone number belongs to several patients. Pick one from candidates and send with patient_id. See Send messages to patients. |
campaign_not_editable | 409 | The campaign’s status doesn’t allow that change. See Campaigns. |
campaign_not_startable | 409 | The campaign is already running, completed, stopped, or archived. |
campaign_not_running | 409 | Only a running campaign can be paused. Only a running or paused campaign can be stopped. |
campaign_not_deletable | 409 | Only a draft campaign can be deleted. Archive a started one instead. |
validation_failed | 422 | Fix each field listed in field_errors. |
campaign_empty | 422 | Add recipients before starting the campaign. |
from_required | 422 | The organization has several phone numbers and no default. Pass from, using a number from List phone numbers. |
no_contact_method | 422 | The patient has no phone number for SMS, or no phone or email for a secure message. Update the patient. |
patient_opted_out | 422 | The patient replied STOP. Send with channel: secure to deliver by email. |
rate_limited | 429 | Wait Retry-After seconds, then retry. |
internal_error | 500 | Retry with backoff. If it persists, contact support with the request_id. |
service_unavailable | 503 | Retry with backoff. |
Handle errors
JavaScript
const response = await fetch(url, options)
if (!response.ok) {
const problem = await response.json()
switch (problem.code) {
case 'invalid_token':
// Refresh the access token and retry once. If refreshing fails, ask the user to reconnect.
break
case 'rate_limited':
await new Promise((r) => setTimeout(r, Number(response.headers.get('Retry-After')) * 1000))
break
default:
throw new Error(`${problem.status} ${problem.code}: ${problem.detail} (request ${problem.request_id})`)
}
}The OAuth token endpoint, https://app.bloomtext.com/oauth/token, is the one exception. It returns standard OAuth errors like invalid_grant instead of problem details. See Token endpoint errors.
Every response, success or error, includes an X-Request-Id header with the same value as request_id. Log it alongside your own request logs.