ChangelogEdit pageSTAFFOpen dashboard

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#

RESPONSE
{
  "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."]
    }
  }
}
  • code is a fixed, machine-readable name for the error. Branch on it, not on message.
  • message explains the problem in plain English, for your logs or for a person.
  • fields only appears on errors about the input: validation_failed, invalid_parameter and limit_exceeded.

Error codes#

StatusCodeWhat it meansWhat to do
401missing_tokenNo Authorization: Bearer header.Send the token.
401invalid_tokenThe token isn't known: mistyped or revoked.Check the token, or create a new one.
401token_expiredThe token is past its expiry date.Create a new token.
403wrong_environmentA Sandbox token on the Production address, or the other way round.Use the address in the message.
403workspace_pausedThe workspace is scheduled for deletion.Contact your workspace's owner.
403platform_api_not_enabledIn Production, the full API isn't turned on yet; only reporting works.Contact customer services.
403missing_abilityThe token doesn't have the ability this endpoint needs.Create a token with that ability.
403limit_exceededThe change would go over one of your plan's limits.Remove something, or upgrade your plan.
403trial_endedYour AWS Marketplace free trial has ended, so you can't create or publish campaigns.Buy through AWS Marketplace.
404not_foundThere's no such resource in this workspace, or no such endpoint.Check the id and the path.
409request_in_progressA request with the same Idempotency-Key is still running.Retry after Retry-After seconds.
422validation_failedThe request body isn't valid.Fix the fields listed in fields.
422invalid_parameterA query parameter isn't valid, such as updated_since.Fix the parameter listed in fields.
422invalid_cursorThe cursor wasn't one Bubbl gave you.Start again from the first page.
429rate_limitedToo many requests or changes.Slow down and retry. See Rate limits.
429too_many_failed_attemptsToo 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:

RESPONSE
{
  "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#

CURL
# -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"}'
JAVASCRIPT
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}`);
  }
}
PYTHON
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']}")