Skip to content

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.

An error is a problem details object (RFC 9457), sent with the content type application/problem+json.

Response: 400 Bad Request
{
"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 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.
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

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 Requests
Retry-After: 60
Content-Type: application/problem+json

A batch is one request however many records it holds, so sending records in batches is the way to send a lot of them.

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 Unchanged in a batch, 200 OK for 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.

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.