|
|
Handle Errors, Rate Limits and Retries
The HTTP status of every response matches the status_code in its body. On top of that, every error says what kind
of error it is, which parameters it is about, and which request it was.
What an error looks like{"status_code": 400, "status": "Bad Request", "message": "Error retrieving event(s)", "errors": ["Invalid value for the event_id parameter: abc. The event_id parameter must be the numeric id of an event."], "error_code": "invalid_request", "error_params": ["event_id"], "request_id": "3f2c9a7e4b1d4c8e9f0a1b2c3d4e5f60"}
Rate limits
A workspace can make 50 calls per second. Each response says where you stand: X-RateLimit-Limit (calls per second),
X-RateLimit-Remaining (calls left this second) and X-RateLimit-Reset (seconds until the count resets). Over the limit,
the API answers 429 with a Retry-After header: wait that many seconds and send the call again.
A call helper that retries the right way
It retries rate limits, a busy Idempotency-Key and server errors, waits as long as the API asks, and gives every create
an Idempotency-Key so a retry never makes a second record. Errors in the request itself are raised immediately.
import time
import uuid
import requests
BASE = "https://api.exhibitday.com/v1"
def call(method, path, api_key, **kwargs):
headers = {"api_key": api_key}
if method == "POST":
headers["Idempotency-Key"] = str(uuid.uuid4()) # the same key for every retry of this create
for attempt in range(5):
response = requests.request(method, BASE + path, headers=headers, timeout=60, **kwargs)
if response.status_code == 429 or response.status_code >= 500 or (response.status_code == 409 and response.json().get("error_code") == "idempotency_key_in_use"):
time.sleep(int(response.headers.get("Retry-After", 2 ** attempt)))
continue
body = response.json()
if response.status_code >= 400:
raise RuntimeError(f"{body['error_code']} {body['error_params']}: {body['errors']} (request {body['request_id']})")
return body
raise RuntimeError("Gave up after 5 attempts")
statuses = call("GET", "/references/payment_statuses", "YOUR_API_KEY")
print(statuses["records_found"], "payment statuses")
try:
call("GET", "/events", "YOUR_API_KEY", params={"event_id": "abc"})
except RuntimeError as error:
print("Refused as expected:", error)
Poll without downloading the same data again
Every successful GET has an ETag header. Send it back in If-None-Match: while nothing changed, the API answers
304 Not Modified without a body. Pair it with the changed-since filter of
Sync Events and Tasks to Another System
for the lightest polling.
curl -i "https://api.exhibitday.com/v1/references/payment_statuses" -H "api_key: YOUR_API_KEY" -H 'If-None-Match: "ETAG_FROM_THE_LAST_RESPONSE"' |