MayaBooks API v1

A small REST API for reading and creating the records you'd otherwise type in by hand, plus signed webhooks so your system hears about things as they happen.

Base URL https://books.maya123.com/api/v1
Auth Authorization: Bearer <your key>
Content type application/json — request and response.

What this API deliberately cannot do

Worth reading before you design against it:

Getting a key

In MayaBooks: Settings → API & Webhooks (Owner role). Choose Read only or Read and create, and optionally an expiry and a per-minute rate limit.

The key is shown once. Only a hash is stored, so it cannot be shown again — if you lose it, revoke it and issue another. Send it in the Authorization header, never in a query string (URLs end up in logs, history and Referer headers).

Check it works

curl -s https://books.maya123.com/api/v1/me \
  -H "Authorization: Bearer mbk_live_xxxxxxxx_xxxxxxxx…"
{
  "data": {
    "company": { "id": 1, "name": "Acme Traders", "gstin": "07AAAAA0000A1Z4",
                 "country": "IN", "base_currency": "INR" },
    "key": { "name": "Website sync", "prefix": "mbk_live_ab12cd34", "scopes": "read,write" },
    "rate_limit": { "per_minute": 60, "used_last_minute": 1 }
  }
}

Endpoints

VerbPathWhat it does
GET/pingLiveness. The only endpoint that needs no key.
GET/meWhich company and key you are using.
GET/partiesCustomers and suppliers. ?type=Customer|Supplier, ?limit=
GET/parties/{id}One party.
POST/partiesCreate a party.
GET/ledgersChart of accounts.
GET/ledgers/{id}One ledger.
POST/ledgersCreate a ledger.
GET/itemsProducts and services.
GET/items/{id}One item.
POST/itemsCreate an item.
GET/sales-invoicesInvoices. ?status=, ?party_id=, ?date_from=, ?date_to=
GET/sales-invoices/{id}One invoice, with its lines.
POST/sales-invoicesCreate an invoice (draft, or posted with "post": true).
GET/paymentsReceipt/payment vouchers. ?type=receipt|payment
GET/payments/{id}One voucher, with its lines.
POST/paymentsRecord money in or out.

Create an invoice

Omit post (or send false) to leave it as a draft, which is the default because a draft is reversible and a posted invoice is not. Tax is always recomputed server-side from quantity, rate, discount and GST rate — amounts you send as totals are ignored.

curl -s -X POST https://books.maya123.com/api/v1/sales-invoices \
  -H "Authorization: Bearer $MB_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "party_id": 12,
    "invoice_date": "2026-07-30",
    "post": true,
    "items": [
      { "description": "Consulting, July", "quantity": 10, "rate": 2500, "gst_rate": 18 },
      { "item_id": 4, "description": "Licence", "quantity": 1, "rate": 9000, "gst_rate": 18 }
    ]
  }'

Record a payment

Give the bank/cash ledger and either the party's ledger or the party id. type is receipt (money in, the default) or payment (money out).

curl -s -X POST https://books.maya123.com/api/v1/payments \
  -H "Authorization: Bearer $MB_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "receipt",
    "date": "2026-07-30",
    "amount": "29500.00",
    "bank_ledger_id": 7,
    "party_id": 12,
    "reference": "NEFT N123456789",
    "post": true
  }'

Errors and limits

StatusMeaning
401Missing or invalid key. One generic message for every auth failure, on purpose.
403A read-only key tried to write, or the key's owner lost access to the company.
404No such record in your company, or no such endpoint.
405A verb other than GET/POST. There is no destructive surface.
422The request was understood but the data was rejected — the message says why.
429Rate limited. See Retry-After.
503The API isn't enabled on this installation yet.
{ "error": { "code": "invalid_request", "message": "An invoice needs at least one line in \"items\"." } }

Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. Amounts are decimal strings, not floats — a float can't hold 0.1 exactly and an accounting API shouldn't invite rounding drift.

Webhooks

Subscribe an https endpoint in Settings → API & Webhooks and we POST a signed JSON body when an event happens.

EventFires when
invoice.createdA sales invoice was created (draft)
invoice.postedA sales invoice was posted (financially committed)
payment.receivedA receipt voucher was posted (money in)
POST /your/endpoint
Content-Type: application/json
Webhook-Id: whd_1234
Webhook-Timestamp: 1785400000
Webhook-Signature: v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-MayaBooks-Event: invoice.posted

{
  "event": "invoice.posted",
  "created_at": "2026-07-30T09:15:00+00:00",
  "company_id": 1,
  "data": { "invoice": { "id": 88, "invoice_no": "INV/2026-27/0042", "grand_total": "29500.00", … } }
}

Verifying the signature — do this before trusting anything

The signature is HMAC-SHA256 over the exact string <Webhook-Id>.<Webhook-Timestamp>.<raw body>, keyed with your signing secret, hex-encoded. The id and timestamp are inside the signed string so you can reject both a replay of this delivery and an attempt to pass off a different one as this.

<?php
$secret  = getenv('MAYABOOKS_WEBHOOK_SECRET');
$body    = file_get_contents('php://input');
$id      = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$ts      = (int) ($_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? 0);
$header  = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';

// Reject anything older than 5 minutes — that is your replay guard.
if (abs(time() - $ts) > 300) { http_response_code(400); exit; }

$expected = hash_hmac('sha256', $id . '.' . $ts . '.' . $body, $secret);
$ok = false;
foreach (explode(' ', trim($header)) as $part) {
    // hash_equals, NOT ==, so the comparison is constant-time.
    if (str_starts_with($part, 'v1=') && hash_equals($expected, substr($part, 3))) { $ok = true; }
}
if (!$ok) { http_response_code(401); exit; }

http_response_code(200);   // 2xx = accepted; anything else is retried
Reply 2xx quickly. Anything else is a failure and is retried after 1, 5, 15, 60 and 360 minutes, then dropped. An endpoint that fails 10 times in a row is switched off automatically and has to be re-enabled. Do your slow work after replying.

Deliveries can arrive more than once (a retry after your 200 was lost in transit), so treat Webhook-Id as an idempotency key. Order is not guaranteed.

Notes

MayaBooks — . Questions: contact us.