How the API behaves
Every call in this API behaves the same way. Learn that once, and the rest of the reference is just field names.
Reading a lot at once
Lists page with a cursor and don't carry a total. Counting a large company's records costs about
as much as fetching the page, on every page, for a number that's out of date by the time you read
it. Follow the Link header until
it stops appearing.
Only what changed
Ask for what moved since last time with updatedSince. Store the newest updatedAt
you saw and send it back on the next run.
GET https://app.obraledger.com/api/public/v1/contacts?updatedSince=2026-08-21T09:30:00Z The bound includes the instant you give it, so nothing slips through the gap between one run and the next. The trade is that you'll see a few records twice. Write your side so a repeat is harmless, which is worth doing anyway.
Money
Amounts come back as strings, not numbers: "1234.5600". Most JSON parsers turn a number into a float, and floats lose cents. Keep the string, or hand
it to something that does decimal arithmetic.
We don't round them to your display setting either. How many decimal places you show is a decision about your screens, and applying it here would throw away precision you might need. You'll get the number we stored.
Writing twice by accident
Anything that creates a record needs an Idempotency-Key: a value you pick, unique to that request. A UUID per request is the usual choice.
Retrying a request whose answer got lost is normal. Without a key, that retry is a second contact. Send the same key again and you get the first answer back instead. Use a different key for a genuinely different request - reusing one with a changed body is refused rather than guessed at, because guessing would leave you believing a record exists that never got created.
Rate limits
Limits apply per key, so two integrations don't compete for one budget. Every response tells you
what's left in X-RateLimit-Remaining and when it refills. Pace off those rather than finding the ceiling by hitting it. If you do hit
it, Retry-After says how many
seconds to wait.
When a call fails
Failures come back as a problem document with a type naming what went wrong. Match on that, not on the message, which we may reword. The status on its
own isn't enough: a 403 covers both "this key can't do that" and "this record won't allow it".
{
"type": "https://www.obraledger.com/api/errors/scope_required",
"title": "Forbidden",
"status": 403,
"detail": "This key was not issued for CONTACT.VIEW."
} That address opens. Every code we can return is listed, with what to do about it and whether trying again helps.
When we change something
The version is in the address, and it's currently 1.0.0. We add fields without changing it, so ignore any field you don't recognize rather than treating it as an error. Taking a field away or changing its type means a new version, and we'll say so before it happens.
Not to be confused with the API our own screens use. That one changes whenever we redesign something, and it isn't promised to anybody. This is the one to build on.
Building something and stuck? Tell us - we'd rather hear it than have you guess.