Lapis API reference

Issue, send, pay, cancel and verify business paper from your own systems. Version 2026-10-01.

Base address https://api.antarax.ai/lapis/v1·openapi.json

The same calls also answer directly at https://angka.antarax.ai/api/v1/lapis.

Start in 5 minutesLapisan Connect guide

Start in 5 minutes

  1. Sign up on the Lapisan Console with your work email. A link signs you in.
  2. Copy your test key. A test key (ax_test_…) and a sandbox business are ready at once. Keep the key on your server, never in a web page or an app.
  3. Make your first call. List the sandbox business's documents, then make an invoice. With a test key every write is a dry run: it answers as live would, with "test": true, and keeps nothing.
export LAPIS_KEY=ax_test_...

curl "https://api.antarax.ai/lapis/v1/documents?limit=5" -H "Authorization: Bearer $LAPIS_KEY"

curl https://api.antarax.ai/lapis/v1/documents \
  -H "Authorization: Bearer $LAPIS_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "invoice", "customer": { "name": "Kedai Runcit Ali" }, "lines": [ { "description": "Roll-up banner", "quantity": 3, "unit_price": "150.00" } ] }'
  1. See it in the Console. Your calls show in the Lapisan Console, under your sandbox business. When you are ready, ask for live access there; AntaraX approves it and you make live keys (ax_live_…).

Already on Lapis? Keep making keys in Lapis, under Settings → Developers, or link your Lapis account in the Console with the same email.

Building for many businesses at once? Read the Lapisan Connect guide: each business approves your app once, and you call for them with your own key.

Getting a key

Sign up on the Lapisan Console for a test key and a sandbox business at once; live keys come once AntaraX approves your request there. A Lapis account's owner can also make keys in Lapis, under Settings → Developers, and link that account in the Console with the same email. A key is shown once, when it is made; keep it on your server and never in a browser or an app.

Each key has the permissions it was given and nothing more:

  • lapis:documents:read to read documents and their PDFs, and to verify documents
  • lapis:documents:write to make, send and cancel documents
  • lapis:payments:write to record payments
  • lapis:customers:read to read customers and their statements
  • lapis:webhooks:write to make and change webhook endpoints, and to read events
  • lapis:connections:read and lapis:connections:write, for Lapisan Connect: to see and end the connections other accounts gave your apps, and to swap an approval's code

Authentication

Call https://api.antarax.ai/lapis/v1, then the path. Send the key as a bearer token on every call. Bodies are JSON.

curl https://api.antarax.ai/lapis/v1/documents -H "Authorization: Bearer ax_live_..."

Every answer carries X-Request-Id (quote it when you ask us about a call) and AntaraX-Version, the date of the shape you are getting.

The key goes in the header and nowhere else. A key in the address (?key=, ?api_key=, ?token= and the like) is refused with 400 key_in_url without being looked up, because addresses end up in logs: stop that key and make another. Bodies over 1 MB are refused with 413 payload_too_large.

Test and live

Keys start ax_test_ or ax_live_. A test key reads the account's real documents and customers, and every write is a dry run: it is checked and worked out exactly as a live call would be, and answered in the same shape with "test": true. Nothing is kept and nobody is emailed. A dry run counts what live would have used (a document issued, an email, a payment) as test usage, so you can see what live would cost; test usage is never capped or billed. A dry run has no url or pdf, since nothing was made, and its events go to the account's test endpoints.

A live key does the real thing: documents are numbered and kept, emails go, money is recorded.

Money, dates and ids

  • Money is a string with two decimals beside an ISO currency: "total": "486.00", "currency": "MYR". Send it the same way. Never a number, so nothing is lost to rounding.
  • Days are YYYY-MM-DD. Moments are ISO 8601 in UTC.
  • Ids carry their kind: ldoc_ for documents, lpay_ for payments, lcus_ for customers.
  • Lists are { "data": [...], "next": "..." }. Pass next back as cursor until it is null.
  • Fields this API does not know are refused, so a misspelt one never passes quietly.

Errors

Every error has the same shape, and its message is a sentence a person can act on.

{ "error": { "code": "number_taken", "message": "INV-2026-0088 is already on another invoice. The next free number is INV-2026-0089.", "request_id": "req_..." } }
StatusCodesMeaning
400key_in_url, invalid_json, invalid_cursor, invalid_account, invalid_codeA key in the address (refused unread: stop that key and make another), a body that is not JSON, a cursor this API did not give out, an AntaraX-Account that is not an account id, or a Connect code that is unknown, used, late, or not yours.
401missing_key, invalid_key, revoked_keyNo key, a key that is not known, or one that was stopped. The answer carries WWW-Authenticate: Bearer realm="antarax".
403missing_scope, wrong_product, api_not_enabled, account_stopped, account_suspended, connection_missing, connection_scopeThe key may not do this, the API is not on for the account, or the account is suspended (it can still be read). With AntaraX-Account: no working connection to that account in the key's mode, or the account did not grant this.
404not_foundNo such document or customer on this account.
409number_taken, paid, cancelled, already_cancelled, not_issued, approval_required, conflict, idempotency_in_progress, endpoint_limitThe document's state does not allow it, the first call with this Idempotency-Key is still running, or the account has its 10 webhook endpoints. The message says what to do.
413payload_too_largeThe body is over 1 MB (15 MB for verify, which carries a PDF).
422invalid_request, incomplete, more_than_owed, branding_missing, no_email, idempotency_mismatchThe body is wrong. The message names the field.
429rate_limited, cap_reached, send_limitToo many calls; wait for Retry-After.
503switched_off, mail_unavailable, unavailablePaused for now, with the reason. Try again later.

Limits and caps

Each key may make so many calls a minute, set by the account's plan; X-RateLimit-Limit and X-RateLimit-Remaining say where you are. Over it, the answer is 429 with Retry-After in seconds.

Live calls also count towards a daily and a monthly allowance for the account. Where the cap is hard, calls past it get 429 cap_reached; where it is soft, they are answered with an AntaraX-Cap-Warning header. Test calls are never capped.

AntaraX can pause the API for everyone, for Lapis, for one account or for one key; paused calls get 503 switched_off with the reason.

Repeats

Send an Idempotency-Key header on a write (any string you choose, up to 120 characters, such as your own order id). If the same call is sent again with the same key and the same body, you get the first answer back, with Idempotent-Replayed: true, and nothing is made twice. The same key with a different body is refused with 422 idempotency_mismatch. A copy that arrives while the first is still running gets 409 idempotency_in_progress: wait for Retry-After and send it again.

Webhooks

Lapis can tell your systems when something happens, instead of you asking. Make an endpoint in Settings → Developers or with POST /webhook_endpoints, and each event is sent to it as a POST with a JSON body, one event a request.

EventWhen
lapis.document.issuedA document was issued
lapis.document.sentA document was emailed
lapis.document.viewedA customer opened a document's link
lapis.payment.recordedA payment was recorded
lapis.document.cancelledA document was cancelled
lapis.connection.createdAn account connected one of your apps (Lapisan Connect)
lapis.connection.revokedA connection to one of your apps ended (Lapisan Connect)
lapis.testA test, sent from the console or the API

Every event has the same envelope. data.object is the document as it was at that moment, in the same shape the API answers with; lapis.payment.recorded carries the invoice after the payment. Events come from everything that happens to the account's paper, in Lapis itself and through the API alike.

POST /hooks/lapis
Content-Type: application/json
AntaraX-Signature: t=1791015262,v1=5f1c0e...a94b

{
  "id": "evt_2c8d41f0a9b34e6c8d7e1f2a3b4c5d6e",
  "type": "lapis.payment.recorded",
  "created": "2026-10-05T08:14:22Z",
  "api_version": "2026-10-01",
  "product": "lapis",
  "tenant_id": "lacc_...",
  "data": { "object": { "id": "ldoc_m1x2k3", "object": "document", "status": "paid", "owed": "0.00", ... } }
}

Check the signature. AntaraX-Signature is t= the time in Unix seconds and v1= the HMAC-SHA256, in hex, of the time, a full stop and the raw body, keyed with the endpoint's secret (ax_whsec_…, shown once when the endpoint is made). Turn away anything that does not match or is more than five minutes old. In Node:

import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

// Checks a delivery against the raw body, exactly as it arrived.
function verifyLapisWebhook(rawBody, header, secret, toleranceSeconds = 300) {
  let t = null;
  const signatures = [];
  for (const part of (header ?? "").split(",")) {
    const [k, v] = part.trim().split("=", 2);
    if (k === "t") t = Number(v);
    if (k === "v1") signatures.push(v);
  }
  if (!t || signatures.length === 0) return false;
  // Older than five minutes: a replay, not a delivery.
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  return signatures.some((s) => {
    const given = Buffer.from(s, "hex");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

const app = express();

// Keep this route's body raw: parsed and printed again, it would not match.
app.post("/hooks/lapis", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyLapisWebhook(raw, req.get("AntaraX-Signature"), process.env.LAPIS_WEBHOOK_SECRET)) return res.sendStatus(400);
  const event = JSON.parse(raw);
  // At least once: skip an event.id you have handled already.
  if (event.type === "lapis.payment.recorded") {
    const invoice = event.data.object; // the document, as the API shows it
    console.log(invoice.number, invoice.status, invoice.owed);
  }
  res.sendStatus(200); // answer quickly; do slow work afterwards
});
  • Answer with any 2xx within 10 seconds. Redirects are not followed: give the final address.
  • Delivery is at least once and in no promised order: de-duplicate on id, and trust created and the object's own state over the order things arrive in.
  • A delivery that fails is tried again about 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours after the event, then given up. Retries run on a 10-minute schedule, so each comes at the first run after its time: up to 10 minutes later than the figure.
  • An endpoint where 20 deliveries in a row fail is switched off, with the reason, until you switch it on again. Its log shows every try, with the code and the time, and Retry now.
  • Test keys and test endpoints go together: a dry run's events go to test endpoints only, and live events to live endpoints only.
  • Events and their deliveries are kept 30 days; GET /events lists them for catching up.

Lapisan Connect: act for other Lapis accounts

For software that works for many businesses, such as a point of sale, a booking system or an accounting tool. Each business keeps its own Lapis account and approves your app once; from then on you call with your own key and say which account with one header. Their paper stays theirs: numbered in their sequence, in their look, emailed from them. Lapisan Connect is part of Lapisan, the AntaraX developer platform; Lapis is the first product on it, and others follow the same steps.

  1. Register an app in Lapis, under Settings → Developers → Lapisan Connect (your account needs the API on): its name, website and logo, the addresses people go back to (matched exactly; http://localhost only for test), and what it asks each account for. Its client_id is app_….
  2. Send the owner to the authorize link. The page is Lapisan's, on Lapis's host for now (it moves to lapisan.antarax.ai/connect once AntaraX has one shared sign-in). They sign in to Lapis by email, or get a free account on the spot, and see your app, the business it will act for, each permission in plain words, and test or live. It never shows inside a frame.
  3. Approve sends them to your redirect_uri with code and your state; Cancel with error=access_denied. A bad link (unknown app, an address you did not register, no state) is explained on the page and sends nobody anywhere.
  4. Swap the code on your server with POST /connect/token and your key. The answer is the connection, with the account id to keep.
  5. Call for them with AntaraX-Account: <account>. The call runs as their account, with what both your key and their approval allow.
1. Your app sends the account's owner to:
   https://angka.antarax.ai/lapis/connect/authorize?client_id=app_...
     &redirect_uri=https://yourapp.com/lapis/callback
     &scope=lapis:documents:read lapis:documents:write
     &state=<a fresh random value you keep>
     &mode=live            (or test)

2. They sign in to Lapis (a new address gets its free account),
   see your app and what it asks for, and press Approve.

3. Lapis sends them back to:
   https://yourapp.com/lapis/callback?code=axc_...&state=<yours>
   (Cancel: ?error=access_denied&state=<yours>)

4. Your server checks state, then swaps the code (10 minutes, once):
   POST https://api.antarax.ai/lapis/v1/connect/token
     { "code": "axc_...", "redirect_uri": "..." }
   -> { "object": "connection", "account": "lap_...", ... }

5. Every call for them: your own key, and their account.
   Authorization: Bearer ax_live_...   (yours)
   AntaraX-Account: lap_...            (theirs)
curl https://api.antarax.ai/lapis/v1/documents \
  -H "Authorization: Bearer $PARTNER_KEY" \
  -H "AntaraX-Account: lap_mh2k3x9" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-8812" \
  -d '{ "type": "invoice", "customer": { "name": "Kedai Runcit Ali" }, "lines": [ { "description": "Banner", "unit_price": "150.00" } ] }'
  • Test and live. A connection is one or the other, as the link said (mode=test), and works only with keys of the same mode. A test connection reads the account's real paper, and everything your test key makes for it is a dry run, as always.
  • Limits and paying. Calls you make for connected accounts count against your own plan (rate per key, daily and monthly caps), not theirs. While the pilot is free, nothing is charged.
  • Repeats. An Idempotency-Key is kept per account you act for, so the same key sent for two accounts runs once for each.
  • Your own things stay yours. Webhook endpoints, events and connections are always your own account's; with AntaraX-Account they answer 403 connection_scope.
  • Disconnects. The owner can disconnect in Lapis, under Settings → Connected apps (with a reason, which you are sent); you can with DELETE /connections/{id}; AntaraX can, and can pause an app. After that, calls get 403 connection_missing. A suspended account turns every connected call away.
  • Webhooks for connected accounts. Their events also go to your own endpoints in the same mode, when the connection may read what they carry: tenant_id is their account, and the envelope adds "connection": "conn_…". lapis.connection.created and lapis.connection.revoked go to your endpoints only, carrying the connection.
  • Errors. 400 invalid_account (not an account id), 400 invalid_code (a code unknown, used, late, for another app or address, or the other mode), 403 connection_missing (no working connection in the key's mode, or the app is paused), 403 connection_scope (not granted).

Use Lapis from Claude or another AI assistant

Lapis is also an MCP server, on the same keys: https://api.antarax.ai/lapis/v1/mcp. An assistant that speaks MCP over HTTP can list and read documents, make and send them, record payments, cancel, read customers and their statements, and verify paper. Each tool needs the same permission as its endpoint here, and counts the same; with a test key every write is a dry run, which is the safe way to start.

For Claude Code, in the project's .mcp.json (or the same entry in another client's settings):

{
  "mcpServers": {
    "lapis": {
      "type": "http",
      "url": "https://api.antarax.ai/lapis/v1/mcp",
      "headers": { "Authorization": "Bearer ${LAPIS_KEY}" }
    }
  }
}

Or from the command line:

claude mcp add --transport http lapis https://api.antarax.ai/lapis/v1/mcp --header "Authorization: Bearer ax_test_..."

The tools are list_documents, get_document, create_document, send_document, record_payment, cancel_document, list_customers, customer_statement and verify_document, each with the input this reference describes. Give the assistant a key with only the permissions it needs: a key that may only read cannot be talked into issuing anything.

POST/documents

Needs lapis:documents:write

Make a document: a quotation, proforma, invoice, credit or debit note, purchase order, payment voucher or delivery order. It is issued in the account's saved look, numbered, signed and given its customer link.

  • customer.name and at least one line are needed. Each line has a description and a unit_price as a string; quantity defaults to 1, tax_percent and discount_percent to 0.
  • number defaults to the next in the account's own sequence. A number already used is refused with 409 number_taken, and the message names the next free one.
  • date defaults to today where the business is. Give due_date, or due_in_days; otherwise an invoice is due in 30 days and a quotation is valid for 14.
  • currency is an ISO code and defaults to the account's own. issue: false keeps a draft to finish in Lapis instead.
  • send_to_customer: true emails it to customer.email once it is issued. If it could not go, the answer says why in notice.
  • Accounts where paper is approved by someone first answer 409 approval_required: send issue: false and approve it in Lapis.

Request

curl https://api.antarax.ai/lapis/v1/documents \
  -H "Authorization: Bearer $LAPIS_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1001" \
  -d '{
    "type": "invoice",
    "customer": { "name": "Kedai Runcit Ali", "email": "ali@kedai.my" },
    "lines": [
      { "description": "Roll-up banner", "quantity": 3, "unit": "pcs", "unit_price": "150.00", "tax_percent": 8 }
    ],
    "due_in_days": 14,
    "send_to_customer": true
  }'

Answer

201 Created
{
  "id": "ldoc_m1x2k3",
  "object": "document",
  "type": "invoice",
  "number": "INV-2026-0088",
  "status": "issued",
  "date": "2026-10-03",
  "due_date": "2026-10-17",
  "currency": "MYR",
  "customer": { "id": "lcus_5f0c3a9be21d7e44", "object": "customer", "name": "Kedai Runcit Ali", "email": "ali@kedai.my", ... },
  "lines": [
    { "description": "Roll-up banner", "quantity": 3, "unit": "pcs", "unit_price": "150.00",
      "discount_percent": 0, "tax_percent": 8, "amount": "486.00" }
  ],
  "subtotal": "450.00", "discount": "0.00", "tax": "36.00", "total": "486.00",
  "paid": "0.00", "owed": "486.00",
  "url": "https://angka.antarax.ai/d/k7m2p9qrst4x",
  "pdf": "https://api.antarax.ai/lapis/v1/documents/ldoc_m1x2k3/pdf",
  "sent_to_customer_at": "2026-10-03T02:14:09.000Z",
  ...
}

GET/documents

Needs lapis:documents:read

This account's documents, drafts included, newest first.

  • Filters: type, status (draft, awaiting_approval, issued, part_paid, paid, cancelled, replaced), customer (part of the name, any case), from and to (issue dates).
  • limit is 1 to 100, 25 by default. Pass next back as cursor for the following page; it is null on the last.

Request

curl "https://api.antarax.ai/lapis/v1/documents?type=invoice&status=part_paid&limit=50" -H "Authorization: Bearer $LAPIS_KEY"

Answer

200 OK
{
  "data": [ { "id": "ldoc_m1x2k3", "object": "document", "number": "INV-2026-0088", ... } ],
  "next": "WyIyMDI2LTEwLTAzVDAyOjE0OjA5LjAwMFoiLCJsZG9jX20xeDJrMyJd"
}

GET/documents/{id}

Needs lapis:documents:read

One document. An invoice comes with its payments. A document of another account is 404 not_found, the same as one that does not exist.

Request

curl https://api.antarax.ai/lapis/v1/documents/ldoc_m1x2k3 -H "Authorization: Bearer $LAPIS_KEY"

Answer

200 OK
{
  "id": "ldoc_m1x2k3", "status": "part_paid", "total": "486.00", "paid": "200.00", "owed": "286.00",
  "payments": [
    { "id": "lpay_m1y9a0", "object": "payment", "amount": "200.00", "currency": "MYR",
      "paid_on": "2026-10-05", "method": "duitnow", "reference": "88213", "receipt": null, ... }
  ],
  ...
}

GET/documents/{id}/pdf

Needs lapis:documents:read

The PDF, as issued, with its signed record inside. A draft is drawn with DRAFT across every page and no record.

Request

curl https://api.antarax.ai/lapis/v1/documents/ldoc_m1x2k3/pdf -H "Authorization: Bearer $LAPIS_KEY" -o INV-2026-0088.pdf

Answer

200 OK
Content-Type: application/pdf

%PDF-1.7 ...

POST/documents/{id}/send

Needs lapis:documents:write

Email an issued document, to the customer's address on it or to another. The same email the customer gets from Lapis.

  • One address receives at most 20 documents a day; over that is 429 send_limit.

Request

curl https://api.antarax.ai/lapis/v1/documents/ldoc_m1x2k3/send \
  -H "Authorization: Bearer $LAPIS_KEY" -H "Content-Type: application/json" \
  -d '{ "to": "accounts@kedai.my" }'

Answer

200 OK
{ "object": "delivery", "sent_to": "accounts@kedai.my", "document": { "id": "ldoc_m1x2k3", ... } }

POST/documents/{id}/payments

Needs lapis:payments:write

Money that arrived against an invoice. amount is what came this time, not the running total; the invoice's status follows the arithmetic.

  • More than is owed is refused with 422 more_than_owed.
  • method is one of bank_transfer, duitnow, qr_payment, cash, card, cheque, online_payment, other. paid_on defaults to today where the business is.

Request

curl https://api.antarax.ai/lapis/v1/documents/ldoc_m1x2k3/payments \
  -H "Authorization: Bearer $LAPIS_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: payment-88213" \
  -d '{ "amount": "200.00", "paid_on": "2026-10-05", "method": "duitnow", "reference": "88213" }'

Answer

201 Created
{
  "object": "payment_recorded",
  "payment": { "id": "lpay_m1y9a0", "amount": "200.00", "currency": "MYR", "paid_on": "2026-10-05", ... },
  "document": { "id": "ldoc_m1x2k3", "status": "part_paid", "paid": "200.00", "owed": "286.00", ... }
}

POST/documents/{id}/cancel

Needs lapis:documents:write

Cancel an issued document. It keeps its place in the record, its number is freed, it is never chased, and its link says it was cancelled.

  • A document with money recorded against it is refused with 409 paid: make a credit note instead, so the record stays true.

Request

curl https://api.antarax.ai/lapis/v1/documents/ldoc_m1x2k3/cancel \
  -H "Authorization: Bearer $LAPIS_KEY" -H "Content-Type: application/json" \
  -d '{ "reason": "Made twice" }'

Answer

200 OK
{ "id": "ldoc_m1x2k3", "object": "document", "status": "cancelled", ... }

GET/customers

Needs lapis:customers:read

Everyone this account has made paper for, most recent first, as they were last written.

  • Lapis keeps no separate customer records: a customer is who the paper names. Their id is lcus_ and the first 16 hex characters of SHA-256 over the name, trimmed, with its spaces collapsed and in lower case, so the same name always has the same id.

Request

curl https://api.antarax.ai/lapis/v1/customers -H "Authorization: Bearer $LAPIS_KEY"

Answer

200 OK
{
  "data": [ { "id": "lcus_5f0c3a9be21d7e44", "object": "customer", "name": "Kedai Runcit Ali",
              "email": "ali@kedai.my", "phone": "", "address": "", "registration": "" } ],
  "next": null
}

GET/customers/{id}/statement

Needs lapis:customers:read

A customer's statement of account: what they owed coming in, every invoice, payment and note in the period with the balance after it, what they owe at the end, and how long each part has been owing.

  • from defaults to their first invoice, to to today. One statement adds up one currency: currency picks it, and currencies lists every one they have invoices in.

Request

curl "https://api.antarax.ai/lapis/v1/customers/lcus_5f0c3a9be21d7e44/statement?from=2026-07-01&to=2026-09-30" -H "Authorization: Bearer $LAPIS_KEY"

Answer

200 OK
{
  "object": "statement",
  "customer": { "id": "lcus_5f0c3a9be21d7e44", "name": "Kedai Runcit Ali" },
  "currency": "MYR", "currencies": ["MYR"],
  "from": "2026-07-01", "to": "2026-09-30",
  "opening": "350.00", "closing": "836.00",
  "entries": [
    { "date": "2026-09-20", "kind": "invoice", "description": "Invoice INV-2026-0088, due 4 Oct 2026",
      "amount": "486.00", "balance": "836.00" }
  ],
  "ageing": { "current": "486.00", "days_1_30": "0.00", "days_31_60": "0.00", "days_61_90": "350.00", "over_90": "0.00" }
}

POST/verify

Needs lapis:documents:read

Is this a genuine Lapis document? Send its link, or the PDF as base64. A PDF is checked by the signed record inside it and against the paper Lapis issued; a link must be on a Lapis address. Any Lapis document can be checked, not only this account's.

Request

curl https://api.antarax.ai/lapis/v1/verify \
  -H "Authorization: Bearer $LAPIS_KEY" -H "Content-Type: application/json" \
  -d "{ \"pdf\": \"$(base64 -w0 INV-2026-0088.pdf)\" }"

Answer

200 OK
{
  "genuine": true, "source": "pdf", "reason": null, "cancelled": false,
  "document": { "type": "invoice", "number": "INV-2026-0088", "date": "2026-10-03", "due_date": "2026-10-17",
                "total": "486.00", "currency": "MYR", "customer": "Kedai Runcit Ali", "issuer": "Kilang Kertas Sdn Bhd" }
}

POST/webhook_endpoints

Needs lapis:webhooks:write

Make a webhook endpoint: an https address on the public internet that Lapis sends events to. A test key makes a test endpoint, which gets test events only; a live key makes a live one. The answer carries the signing secret, the one time it is shown.

  • events lists the types to send; left out or empty, every one. See Webhooks above for the list.
  • An account has at most 10 endpoints; another is refused with 409 endpoint_limit. Private, loopback and link-local addresses, localhost, .local and .internal are refused.
  • GET /webhook_endpoints lists them, GET and PATCH /webhook_endpoints/{id} read and change one (url, description, events, enabled), and DELETE removes it with its log.

Request

curl https://api.antarax.ai/lapis/v1/webhook_endpoints \
  -H "Authorization: Bearer $LAPIS_KEY" -H "Content-Type: application/json" \
  -d '{ "url": "https://shop.example.com/hooks/lapis", "events": ["lapis.payment.recorded", "lapis.document.viewed"] }'

Answer

201 Created
{
  "id": "whe_4b1e9c07a2d35f86", "object": "webhook_endpoint",
  "url": "https://shop.example.com/hooks/lapis", "description": "",
  "events": ["lapis.payment.recorded", "lapis.document.viewed"],
  "mode": "live", "enabled": true, "disabled_reason": null, "failure_streak": 0,
  "secret_prefix": "ax_whsec_k7Qm",
  "secret": "ax_whsec_k7Qm...",
  "created_at": "2026-10-03T02:20:11.000Z", "updated_at": "2026-10-03T02:20:11.000Z"
}

POST/webhook_endpoints/{id}/test

Needs lapis:webhooks:write

Send the endpoint a lapis.test event now, whatever it subscribes to, and wait for the answer, so you can check your signature code before anything real happens.

Request

curl -X POST https://api.antarax.ai/lapis/v1/webhook_endpoints/whe_4b1e9c07a2d35f86/test -H "Authorization: Bearer $LAPIS_KEY"

Answer

200 OK
{
  "object": "webhook_test", "delivered": true, "status_code": 200, "error": null, "duration_ms": 182,
  "event": { "id": "evt_9c2f...", "object": "event", "type": "lapis.test", ... }
}

GET/events

Needs lapis:webhooks:write

Recent events in the key's mode, newest first, exactly as they were sent. Use it to catch up after your endpoint was down. GET /events/{id} reads one.

  • Filter with type. limit is 1 to 100; pass next back as cursor. Events are kept 30 days, and only while the account has an endpoint in that mode.

Request

curl "https://api.antarax.ai/lapis/v1/events?type=lapis.payment.recorded&limit=10" -H "Authorization: Bearer $LAPIS_KEY"

Answer

200 OK
{
  "data": [ { "id": "evt_2c8d...", "object": "event", "type": "lapis.payment.recorded", "created": "2026-10-05T08:14:22Z", ... } ],
  "next": null
}

POST/connect/token

Needs lapis:connections:write

Lapisan Connect: swap the code an account's approval sent to your redirect_uri for the connection. Your own key, never the account's.

  • Within 10 minutes, once. The code must be for one of your apps, with the same redirect_uri, and swapped with a key in the same mode (a test code with a test key). Anything else is 400 invalid_code, and the code is spent.
  • Check the state you sent first: it is yours, and it is how you know the answer is for the person you sent.

Request

curl https://api.antarax.ai/lapis/v1/connect/token \
  -H "Authorization: Bearer $PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "axc_...", "redirect_uri": "https://yourapp.com/lapis/callback" }'

Answer

200 OK
{
  "object": "connection",
  "id": "conn_5d1f0a9e2b7c4e11",
  "app": "app_8c2e41f0a9b34e6c",
  "account": "lap_mh2k3x9",
  "account_name": "Kedai Runcit Ali",
  "scopes": ["lapis:documents:read", "lapis:documents:write"],
  "mode": "live",
  "status": "active",
  "created": "2026-10-05T08:14:22Z",
  ...
}

GET/connections

Needs lapis:connections:read

The accounts your apps are connected to, in the key's mode, newest first. GET /connections/{id} reads one.

  • Filter with app (a client_id); status=all includes ended ones. limit is 1 to 100; pass next back as cursor.

Request

curl "https://api.antarax.ai/lapis/v1/connections?limit=50" -H "Authorization: Bearer $PARTNER_KEY"

Answer

200 OK
{ "data": [ { "object": "connection", "id": "conn_5d1f...", "account": "lap_mh2k3x9", ... } ], "next": null }

DELETE/connections/{id}

Needs lapis:connections:write

End a connection from your side. Your app can no longer act for that account until its owner approves it again; your endpoints get lapis.connection.revoked.

Request

curl -X DELETE https://api.antarax.ai/lapis/v1/connections/conn_5d1f0a9e2b7c4e11 -H "Authorization: Bearer $PARTNER_KEY"

Answer

200 OK
{ "object": "connection", "id": "conn_5d1f...", "status": "revoked", "revoked_by": "partner", ... }

The whole shape

Every field of every request and answer is in openapi.json, an OpenAPI 3.1 document made from the same schemas the API checks with. Load it into Postman, Insomnia or a client generator. It needs no key.