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, 202, 204 | Success. A 202 send waits for the patient’s consent to texting. | No |
400 | The request is malformed. | No, fix the request. |
401 | The API key is missing, expired, or revoked (or, with OAuth, the access token is). | No; with OAuth, after refreshing the token. |
403 | The credential lacks the scope, an API key called an OAuth-only endpoint, or API access isn’t turned on for the organization. | No |
404 | The resource doesn’t exist, or the key can’t see it. | No |
405 | The path exists but doesn’t take that method. The Allow header lists the ones it does. | 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 | Look patients up by phone or mrn. Bulk listing isn’t available; contact BloomText if you need a sync. See Patients. |
org_id_required | 400 | The API key reaches more than one organization. Pass org_id, from GET /orgs. See Organizations a key reaches. |
invalid_query | 400 | The q search query can’t be parsed. detail points at the problem. See Search. |
invalid_token | 401 | API key: check you’re sending the whole key. With OAuth: refresh the access token. If the refresh fails with invalid_grant, the admin must reconnect your app. See When a connection ends. |
key_revoked | 401 | An Owner or IT person revoked this API key. Ask them for a new one. See API keys. |
key_expired | 401 | The API key’s expiry has passed. An Owner or IT person can extend it under Settings → Account → API keys, or you can ask for a new one. See Expiry. |
insufficient_scope | 403 | The credential lacks the scope listed on the endpoint’s reference page. Create a key with that scope. |
api_access_disabled | 403 | API access isn’t turned on for your organization. It’s turned on once a BAA is signed. See API keys to request access. |
plan_required | 403 | The organization’s plan doesn’t include patient messaging, so it can’t send to patients. Contact BloomText to add it. Everything else, like patient lookups, still works. |
org_not_reachable | 403 | The API key can’t reach the organization in org_id. GET /orgs lists the ones it can. |
accounts_not_enabled | 403 | The members, teams, and organizations endpoints work once BloomText turns on accounts for your account. Contact BloomText. |
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, and that the key reaches its organization. |
method_not_allowed | 405 | Use a method from the Allow header. |
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. |
recipient_mismatch | 409 | patient.mrn belongs to a patient with a different phone number, or the number’s only patient has a different MRN. Nothing was sent. Update the patient in candidates, or send with patient_id. See Send messages to patients. |
already_member | 409 | The person is already a member of that organization. |
patient_account | 409 | The email belongs to a patient’s BloomText account, which can’t be a staff member. |
managed_by_admin | 409 | People in BloomText manage this membership or put this person on the team, so the API can’t change it. See Who manages a membership. |
managed_by_account | 409 | This is an Owner or IT person’s membership, set on the Account page in BloomText. |
managed_by_identity_provider | 409 | The account’s Okta rules manage this membership or team placement. Change it in Okta or in the rules. See SSO and Directory Sync. |
confirmation_required | 409 | With reason: would_delete_user: removing this person deletes their BloomText user. Repeat with confirm_delete_user=true to do that. See Remove a member. |
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. |
unknown_field | 422 | The body has a field the endpoint doesn’t take. Each one is in field_errors, and the message says where it goes when it belongs elsewhere. See Unknown fields are rejected. |
campaign_empty | 422 | Add recipients before starting the campaign. |
from_required | 422 | The organization has several phone numbers and no default. detail and numbers list them; set from to one. See Which number it comes from. |
channel_required | 422 | Set channel to sms or secure. There’s no default. See Secure or plain text. |
too_long | 422 | The body is over max characters (1,600 for SMS). length says how long it was. Shorten it, split it yourself, or send it secure. |
patient_has_no_phone | 422 | An SMS to a patient with no phone number. Add one, or send secure if they have an email address. |
no_contact_method | 422 | A secure message to a patient with neither phone nor email. Update the patient. |
contact_cap_reached | 422 | The organization has texted as many new patients as it can this billing period until BloomText verifies it. Contact support to get verified. |
patient_opted_out | 422 | The patient replied STOP to your organization, or staff recorded their opt-out (since says when), so no number can text them until they reply START. Stop texting them; a secure message still reaches them by email. See Opt-outs and blocked numbers. |
carrier_blocked | 422 | The carrier refused an earlier text to the patient, so none of your organization’s numbers text them. Stop texting them; they can text START to one of your numbers. |
number_blocked | 422 | An admin in your organization blocked the patient’s number, so none of its numbers text them. Stop texting them; only an admin can unblock it, in the app. |
number_unreachable | 422 | An earlier text failed because the patient’s number can’t receive texts: a landline, or a carrier that couldn’t be reached. None of your organization’s numbers text it until the patient texts one of your numbers, their phone number is changed, or an admin clears it in the app. Update the patient’s mobile number, or send secure. |
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. |
Unknown fields are rejected
A POST or PATCH body may only contain the fields its reference page lists. Any other field, such as a typo or a field in the wrong place, returns 422 unknown_field before anything runs. Nothing is sent or changed, and the Idempotency-Key isn’t used up, so fix the body and send it again with the same key. Each unknown field gets an entry in field_errors, with its JSON pointer and where it belongs, if anywhere:
{
"type": "https://www.bloomtext.com/developers/api/errors/#unknown_field",
"title": "Unprocessable Entity",
"status": 422,
"code": "unknown_field",
"detail": "The body has fields this endpoint doesn't take. Unknown fields are rejected rather than ignored, so a typo or a misplaced field never goes unnoticed. Fix each one in field_errors.",
"request_id": "0b6c2d1e-5f4a-4c3b-9a8d-7e6f5a4b3c2d",
"field_errors": [
{ "path": "/patient", "code": "unknown_field", "message": "'patient' isn't a top-level field; put it under 'to'." }
]
}Responses work the other way: they may gain new fields within v1, so ignore fields you don’t recognize. See the Changelog.
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.