ObraLedger

Type to search.

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.

Request
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".

A failed response
{
  "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.