Idempotency
Send an Idempotency-Key header with any change, and if you retry after a timeout or a dropped connection, Bubbl replays the first result instead of making the change again.
A request can time out after Bubbl has done the work but before you get the answer. Retrying it blindly might do the work twice. An idempotency key makes a retry safe: Bubbl remembers the result of the first request and returns it again for any retry with the same key.
Where it works#
Every request that changes something accepts an Idempotency-Key header: every POST, PATCH and DELETE, such as creating a campaign, adding a notification, publishing, pausing or importing locations. GET requests don't change anything, so they don't need one.
Send a key#
Make up a unique value for each change, such as a UUID, and send it in the Idempotency-Key header. Keep the same value for every retry of that change. The examples below import locations, but it works the same way for any change.
curl https://api.bubbl.tech/platform/v1/locations/import \
-H "Authorization: Bearer $BUBBL_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f1c9a52-8d4e-4b7a-9c61-2e5f0d8a7b34" \
-d @stores.geojsonimport { randomUUID } from 'node:crypto';
async function importLocations(featureCollection) {
const key = randomUUID(); // one key per import, reused for its retries
for (let attempt = 1; attempt <= 5; attempt++) {
const response = await fetch('https://api.bubbl.tech/platform/v1/locations/import', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.BUBBL_API_TOKEN}`,
'Content-Type': 'application/json',
'Idempotency-Key': key,
},
body: JSON.stringify(featureCollection),
}).catch(() => null); // a network error: retry
if (response && response.status !== 409 && response.status < 500) {
if (!response.ok) throw new Error(`Import failed: ${response.status} ${await response.text()}`);
return response.json();
}
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));
}
throw new Error('Import failed after 5 attempts');
}import os
import time
import uuid
import requests
def import_locations(feature_collection):
key = str(uuid.uuid4()) # one key per import, reused for its retries
for attempt in range(1, 6):
try:
response = requests.post(
"https://api.bubbl.tech/platform/v1/locations/import",
headers={
"Authorization": f"Bearer {os.environ['BUBBL_API_TOKEN']}",
"Idempotency-Key": key,
},
json=feature_collection,
timeout=60,
)
except (requests.ConnectionError, requests.Timeout):
response = None
if response is not None and response.status_code != 409 and response.status_code < 500:
response.raise_for_status()
return response.json()
time.sleep(2 ** attempt)
raise RuntimeError("Import failed after 5 attempts")What happens on a retry#
- The first request runs as normal. If it succeeds, Bubbl stores its response for 24 hours.
- A retry with the same key within 24 hours gets the stored response back, with the same status (such as
201for a create) and the headerIdempotent-Replayed: true. Nothing is changed again. - A retry while the first request is still running waits for it for up to 10 seconds. If it's still running then, the retry gets
409with the coderequest_in_progressandRetry-After: 2. Retry again after that. - A request that fails, for example with
422 validation_failed,403 limit_exceeded,401or429, stores nothing. Fix the problem and retry with the same key. - Replays still count towards your rate limits.
Keys are kept per workspace and per URL path, so another workspace, or a request to a different path, using the same value doesn't clash with yours. The query string isn't part of the path.
Retrying without a key#
Without a key, a retried change runs again. Campaign names are unique in a workspace, so a retried campaign create gets 422 validation_failed for the name if the first one worked, but a retried notification add makes a second copy. Check whether the first request worked (for example, list the campaign's notifications) before retrying. Publish and pause are safe to repeat: publishing a campaign that isn't paused, or pausing one that's already paused, changes nothing.
For location imports, features with properties.external_id are safe to import twice without a key: the second import finds the locations by their external_id and updates them instead of creating copies. A key still saves the import running twice, and protects any features without an external_id.