Developer reference

REST API

A small, predictable API for reading and creating workspace data. Scale tier. Bearer-token auth. JSON in, JSON out.

Auth

Generate keys in Settings → API Keys. Each key looks like ck_live_xxxxxxxx_… and is shown once at creation. Send it as the standard Authorization header on every request:

Authorization: Bearer ck_live_xxxxxxxx_…

Rate limit: 120 requests per minute, per key (30 for a sandbox key). Every response carries RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers (reset in seconds), and a 429 adds Retry-After — throttle on those rather than counting requests yourself. Optionally restrict by IP allowlist in the key settings.

Testing

Try the API without touching your real data: in Settings → API Keys, choose Create sandbox key. You get a key starting ck_test_ that reaches a separate practice company — never your real one — which starts with sample data: 2 clients, 2 projects, 3 invoices (Draft, Sent, Partly paid) and 2 vendors. Every endpoint works there, reads and writes alike.

  • Every reply to a sandbox key carries the header Rebaar-Sandbox: true, so you can tell which side you are on.
  • The practice company sends nothing: no email, no WhatsApp, no webhooks. It has no GSTIN, so invoices there carry 0 % GST.
  • Sandbox keys are limited to 30 requests a minute, and the practice company holds at most 10,000 clients, projects, invoices or vendors each — one more is 402 sandbox_limit.
  • Reset sandbox (same screen, Owner only) deletes everything your sandbox keys wrote and puts the sample data back; the keys keep working.
  • A practice company nobody calls for 90 days is removed; create a sandbox key again to get a fresh one.
  • Every endpoint page below has a Send button that runs the call against your practice company with a sandbox key. It never sends a live key.

Base URL

https://rebaar.in/api/v1

Machine-readable spec: https://rebaar.in/openapi.json (OpenAPI 3.0 — typed schemas and operation IDs for codegen and AI function calling). Every endpoint page below is generated from it, and each has a Markdown copy at the same address with .md on the end.

All amounts are integers in paise (₹ × 100): ₹1,500.00 is 150000. All dates are ISO YYYY-MM-DD strings. Lists return { "object": "list", "data": [...], "hasMore": … }, newest first, with ?limit= from 1 to 200 (default 50).

Endpoints

Clients

Projects

Invoices

Vendors

Need an endpoint that isn't listed? Subscribe to the outbound webhooks for event notifications, or email [email protected].

Webhooks

Outbound webhooks fire on workspace events: invoice.created, invoice.paid, invoice.overdue, project.created, project.status_changed, lead.created, lead.won, quotation.approved, quotation.rejected, and on the Scale plan site_stock.movement_created and site_stock.movement_reversed (one per stock movement of a site or the depot: material, kind, quantity in the material's own unit, the stock after it — never a rate, a value or who entered it).

Configure your endpoint URL in Settings → Webhooks. Every request is signed following the Standard Webhooks specification:

webhook-id:        <unique per message; REUSED across retries — use as your idempotency key>
webhook-timestamp: <unix seconds, when THIS attempt was sent>
webhook-signature: v1,<base64 HMAC-SHA256>
x-rebaar-event:    invoice.paid   (convenience only — NOT signed; never branch on it before verifying)

The signed content is {webhook-id}.{webhook-timestamp}.{raw body}, joined by full stops. Use the raw request body — parsing and re-serialising changes the bytes and the signature will not match. Reject anything whose timestamp is more than 5 minutes from your clock, in either direction; that is what stops a captured request being replayed later.

const crypto = require('crypto')

function verify(req, rawBody, secret) {
  const id = req.headers['webhook-id']
  const ts = Number(req.headers['webhook-timestamp'])
  if (!Number.isFinite(ts)) return false
  if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) return false   // replay window

  const expected = 'v1,' + crypto
    .createHmac('sha256', secret)
    .update(id + '.' + ts + '.' + rawBody)
    .digest('base64')

  // Any entry may match — the header is a space-delimited list so we can rotate secrets.
  return String(req.headers['webhook-signature'])
    .split(' ')
    .some(sig => sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)))
}

We retry failed deliveries with exponential backoff up to 5 attempts, then move them to the dead-letter queue. A retry keeps the same webhook-id but carries a fresh webhook-timestamp and signature, so verification succeeds however late it arrives — deduplicate on webhook-id.

Errors

Every error reply is JSON with an error field; most add a message, and a validation error names the field when it can. Each endpoint page lists its own replies. These can come from any call:

Errors every call can return
StatusCodeMeaningWhat to do
401unauthenticatedNo `Authorization: Bearer …` header was sent.Send your API key as `Authorization: Bearer ck_live_…` on every request.
401invalid_tokenThe key is unknown, revoked or expired — or the company cannot use it.Check you copied the whole key. Make a new one in Settings → API keys if it was revoked or has expired.
403ip_not_allowedThe request came from an IP address that is not on the key's allowlist.Call from an allowed address, or change the key's IP allowlist in Settings → API keys.
402plan_requiredThe company is not on the Scale plan, which includes the API.The Owner moves the company to Scale in Settings → Billing.
402access_expiredThe subscription is not active (trial over, payment failed, cancelled, paused, locked or closing). `reason` says which.The Owner pays or renews in Settings → Billing; calls work again at once.
429—More than 120 requests in a minute with this key (30 with a sandbox key).Wait the number of seconds in the `Retry-After` header. Every reply carries `RateLimit-Remaining`, so you can slow down before this happens.
403insufficient_scopeThe key does not carry the scope this method needs: `read` for GET, `write` for POST, PATCH and DELETE.Make a key with the scope you need in Settings → API keys.
500internalSomething failed on our side.Retry with exponential backoff. If it keeps happening, email [email protected] with the time of the call.