Errors and limits
The API answers with ordinary HTTP status codes. A 2xx means it worked. Anything else comes with a body that says what went wrong.
Read an error
Section titled “Read an error”An error is a problem details object (RFC 9457), sent with the content type application/problem+json.
{ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "sourceRecordId": ["sourceRecordId is required."] }, "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00"}| Field | Meaning |
|---|---|
title |
What went wrong, in a sentence. |
status |
The HTTP status code, again. |
type |
A link to the part of the HTTP standard that defines the status. Not sent with 429. |
errors |
Only on 400, when a field breaks a limit: each field, with what’s wrong with it. In a batch the field is named by the record’s position, counting from 0, such as [2].email. |
traceId |
The ID of this request. Quote it if you ask us about the error. |
Decide what to do from the status code, not from the wording of title, which can change.
Status codes
Section titled “Status codes”| Status | Meaning | What to do |
|---|---|---|
200 OK |
It worked. | Nothing. |
201 Created |
A record, or a snapshot, was created. | Nothing. |
204 No Content |
A delete worked. | Nothing. |
400 Bad Request |
The body isn’t JSON of the right shape, or a field breaks a limit. errors names the fields. |
Fix the request. Retrying it as it is will fail the same way. |
401 Unauthorized |
The X-Api-Key header is missing, or the key isn’t valid: it may have been revoked. |
Check the key. See Authenticate with an API key. |
404 Not Found |
The source has no record with this ID, or no snapshot with this ID. | Check the ID, and that you’re using the key of the right source. |
409 Conflict |
Another request changed the same record at the same moment (rarely, a file being imported into the source). Or, for a snapshot, it isn’t open: it was completed, or a newer one replaced it. | For a record, send the request again. For a snapshot, open a new one. |
413 Payload Too Large |
The body is too big: over 64 KB for one record, or 8 MB for a batch or a snapshot’s records. | Send less in each request. |
415 Unsupported Media Type |
The body wasn’t sent as JSON. | Set Content-Type: application/json. |
429 Too Many Requests |
More than 600 requests a minute with this key. | Wait the number of seconds in Retry-After, then send it again. |
5xx |
Something went wrong on our side. | Wait, then send it again. If it keeps happening, tell us the traceId. |
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Requests | 600 a minute for each API key |
| Records in a batch, or in one request to a snapshot | 1 to 1,000 |
| Size of a request body | 64 KB for one record; 8 MB for a batch or a snapshot’s records |
sourceRecordId |
1 to 100 characters |
| Every other text field | 200 characters |
| Phone numbers in a record | 10 |
| Active API keys for a source | 2 |
| Size of a CSV file | 50 MB |
| Records a snapshot or a complete-list file can delete without an admin’s approval | 10% of the source’s records; a larger deletion is held |
The rate limit
Section titled “The rate limit”Each key can make 600 requests a minute, whatever the requests are. It’s counted for each key, not each source or each address. The count covers a fixed one-minute window.
Over the limit, the API answers 429 Too Many Requests with a Retry-After header: the number of whole seconds to wait, up to 60. Nothing is lost: a request that gets a 429 wasn’t done, so send it again after waiting.
HTTP/1.1 429 Too Many RequestsRetry-After: 60Content-Type: application/problem+jsonA batch is one request however many records it holds, so sending records in batches is the way to send a lot of them.
Retrying safely
Section titled “Retrying safely”Every call that writes can be repeated without harm, so when you don’t know what happened, send it again:
- Sending a record that’s already there changes nothing: the answer is
Unchangedin a batch,200 OKfor one record. - Deleting a record that’s already deleted succeeds.
- Sending the same record to a snapshot again is harmless: the snapshot simply sees it again.
The exceptions are calls you shouldn’t repeat once they’ve worked. Completing a snapshot a second time gets 409 Conflict. Opening a snapshot again discards the earlier one, so open it again only if you mean to start over.
What to retry, and how:
| Answer | Retry? | When |
|---|---|---|
| No answer: a timeout, or a dropped connection | Yes | After a pause, doubling it each time: 1 second, 2, 4, 8. |
5xx |
Yes | The same. |
429 |
Yes | After the number of seconds in Retry-After. |
409 on a record, or when opening a snapshot |
Yes | After a short pause. |
409 when adding records to a snapshot or completing it |
No | The snapshot isn’t open, and that doesn’t change. If the response to completing it was lost, it may have worked: check for a held snapshot under Admin, then Matching, and at the source’s records, before you run the snapshot again. |
400, 401, 404, 413, 415 |
No | The request is wrong, or the key is. Fix it first. |
Stop after a handful of attempts and report the failure, rather than retrying for ever. The first request after a quiet spell can take a minute or more, while the app and its database wake up, so set a client timeout of at least 90 seconds.
This Python function does all of that, and stops at once on a 409 from a snapshot. It needs the requests library (pip install requests):
import time
import requests
BASE = "https://app.samesake.co.uk/api/v1"HEADERS = {"X-Api-Key": "mk_abcd1234_0123456789abcdefghjkmnpqrstvwxyz"}
def send(method, path, **kwargs): """Send one request. Retries a 429 (after Retry-After), a 5xx, a network error, and a 409 on a record call.""" for attempt in range(6): try: response = requests.request(method, BASE + path, headers=HEADERS, timeout=90, **kwargs) except (requests.ConnectionError, requests.Timeout): time.sleep(2 ** attempt) continue status = response.status_code if status == 429: time.sleep(int(response.headers.get("Retry-After", 2 ** attempt))) elif status >= 500 or (status == 409 and not path.startswith("/snapshots/")): time.sleep(2 ** attempt) elif status == 409: # On a snapshot call, 409 means the snapshot isn't open: it was completed, or a newer one replaced it. That # never changes, so don't retry. If the response to "complete" was lost, it may have worked: look for a # held snapshot under Admin > Matching, and check the source's records, before you run this again. raise RuntimeError(f"409 {response.text}: the snapshot isn't open. It may already have completed.") elif status >= 400: raise RuntimeError(f"{status} {response.text}") else: return response raise RuntimeError(f"Gave up on {method} {path}")
response = send("POST", "/records", json={"sourceRecordId": "CRM-10042", "firstName": "Priya", "lastName": "Sharma"})print(response.status_code, response.json()) # 201 {'recordId': 'rec_...'}It gives up after six attempts, and raises an error that includes the response, traceId and all.
Asking us about an error
Section titled “Asking us about an error”Email hello@samesake.co.uk with the traceId, the time, and what you sent, without the API key. See Contact for other ways to reach us.