REST API
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
| Method | Path | What it does |
|---|---|---|
| GET | /clients | List clients |
| POST | /clients/create | Create a client |
| GET | /clients/{id} | Get one client |
| PATCH | /clients/{id} | Update a client |
| DELETE | /clients/{id} | Delete a client |
Projects
| Method | Path | What it does |
|---|---|---|
| GET | /projects | List projects |
| POST | /projects/create | Create a project |
| GET | /projects/{id} | Get one project |
Invoices
Vendors
| Method | Path | What it does |
|---|---|---|
| GET | /vendors | List vendors |
| POST | /vendors/create | Create a vendor |
| GET | /vendors/{id} | Get one vendor |
| PATCH | /vendors/{id} | Update a vendor |
| DELETE | /vendors/{id} | Delete a vendor |
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
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 401 | unauthenticated | No `Authorization: Bearer …` header was sent. | Send your API key as `Authorization: Bearer ck_live_…` on every request. |
| 401 | invalid_token | The 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. |
| 403 | ip_not_allowed | The 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. |
| 402 | plan_required | The company is not on the Scale plan, which includes the API. | The Owner moves the company to Scale in Settings → Billing. |
| 402 | access_expired | The 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. |
| 403 | insufficient_scope | The 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. |
| 500 | internal | Something failed on our side. | Retry with exponential backoff. If it keeps happening, email [email protected] with the time of the call. |