Errors
Every error comes back with an HTTP status and the same JSON shape, with a code you can branch on and, for invalid input, the problem with each field.
The error shape#
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"fields": {
"name": ["The name field is required."],
"location_ids": ["A geo campaign needs at least one location or location group."]
}
}
}codeis a fixed, machine-readable name for the error. Branch on it, not onmessage.messageexplains the problem in plain English, for your logs or for a person.fieldsonly appears on errors about the input:validation_failed,invalid_parameterandlimit_exceeded.
Error codes#
| Status | Code | What it means | What to do |
|---|---|---|---|
| 401 | missing_token | No Authorization: Bearer header. | Send the token. |
| 401 | invalid_token | The token isn't known: mistyped or revoked. | Check the token, or create a new one. |
| 401 | token_expired | The token is past its expiry date. | Create a new token. |
| 403 | wrong_environment | A Sandbox token on the Production address, or the other way round. | Use the address in the message. |
| 403 | workspace_paused | The workspace is scheduled for deletion. | Contact your workspace's owner. |
| 403 | platform_api_not_enabled | In Production, the full API isn't turned on yet; only reporting works. | Contact customer services. |
| 403 | missing_ability | The token doesn't have the ability this endpoint needs. | Create a token with that ability. |
| 403 | limit_exceeded | The change would go over one of your plan's limits. | Remove something, or upgrade your plan. |
| 403 | trial_ended | Your AWS Marketplace free trial has ended, so you can't create or publish campaigns. | Buy through AWS Marketplace. |
| 404 | not_found | There's no such resource in this workspace, or no such endpoint. | Check the id and the path. |
| 409 | request_in_progress | A request with the same Idempotency-Key is still running. | Retry after Retry-After seconds. |
| 422 | validation_failed | The request body isn't valid. | Fix the fields listed in fields. |
| 422 | invalid_parameter | A query parameter isn't valid, such as updated_since. | Fix the parameter listed in fields. |
| 422 | invalid_cursor | The cursor wasn't one Bubbl gave you. | Start again from the first page. |
| 429 | rate_limited | Too many requests or changes. | Slow down and retry. See Rate limits. |
| 429 | too_many_failed_attempts | Too many unknown tokens from your IP address. | Fix the token, then wait Retry-After seconds. |
The token is checked first, so a request with a bad token gets a 401 whatever else is wrong with it.
A resource from your other workspace, or one that's been deleted, is 404 not_found like one that never existed.
Validation errors#
fields maps each field with a problem to a list of messages. The rules and wording are the same as the dashboard's forms, so a request is refused for the same reasons the dashboard would refuse it.
Nested fields use dots, and list items use their position from 0. For example, trigger_config.send_at is the send_at field inside trigger_config, and location_ids.2 is the third id in location_ids.
A plan limit comes back as limit_exceeded, with the limit's name as the field:
{
"error": {
"code": "limit_exceeded",
"message": "You've reached the limit for max_campaigns.",
"fields": {
"max_campaigns": ["Limit is 10, already at 10."]
}
}
}A location import is different: a bad feature doesn't fail the whole request. Each feature gets its own result, with its reasons. See Importing locations with GeoJSON.
Other errors#
A few responses don't use this shape: for example, 405 when an endpoint doesn't accept the HTTP method, or a 5xx if something goes wrong on Bubbl's side. Check the status code before reading error. Treat a 5xx as temporary and retry with backoff; for a retried change, send an Idempotency-Key so it can't run twice.
Handling errors in code#
# -w prints the status after the body; the body holds the error's code and message.
curl -s -w "\n%{http_code}\n" https://api.bubbl.tech/platform/v1/campaigns \
-H "Authorization: Bearer $BUBBL_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Autumn launch", "type": "geo", "timezone": "Europe/London"}'const response = await fetch('https://api.bubbl.tech/platform/v1/campaigns', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.BUBBL_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'Autumn launch', type: 'geo', timezone: 'Europe/London' }),
});
if (!response.ok) {
const body = await response.json().catch(() => null);
const error = body?.error ?? { code: `http_${response.status}`, message: response.statusText };
if (error.code === 'validation_failed') {
for (const [field, messages] of Object.entries(error.fields)) {
console.error(`${field}: ${messages.join(' ')}`);
}
} else {
console.error(`${error.code}: ${error.message}`);
}
}import os
import requests
response = requests.post(
"https://api.bubbl.tech/platform/v1/campaigns",
headers={"Authorization": f"Bearer {os.environ['BUBBL_API_TOKEN']}"},
json={"name": "Autumn launch", "type": "geo", "timezone": "Europe/London"},
timeout=30,
)
if not response.ok:
try:
error = response.json()["error"]
except (ValueError, KeyError):
error = {"code": f"http_{response.status_code}", "message": response.reason}
if error["code"] == "validation_failed":
for field, messages in error["fields"].items():
print(f"{field}: {' '.join(messages)}")
else:
print(f"{error['code']}: {error['message']}")