Docs / API reference

TypeScript SDK

The Invoa TypeScript SDK: a small, dependency-free client for the REST API with typed methods, error handling and webhook signature verification.

The Invoa TypeScript SDK wraps the REST API in typed methods and includes a webhook signature verifier. It has no dependencies and runs anywhere with fetch and Web Crypto: Node.js 20 or later, Deno, Bun and Cloudflare Workers.

Get the SDK

The SDK package is named @invoa/api-sdk. It is not published on the public npm registry; it is available on request from hello@invoa.io. Everything it does is plain HTTPS calls, so you can also call the REST API directly from any language.

Important: Use the SDK on your server only. Never ship an API key to a browser or mobile app.

Set up a client

ts
import { Invoa } from "@invoa/api-sdk";

const invoa = new Invoa({
  apiKey: process.env.INVOA_API_KEY!, // inv_live_… from Settings → API keys
});
OptionTypeDescription
apiKeystringRequired. Your API key.
baseUrlstringDefault https://app.invoa.io. The SDK adds /api/v1.
timeoutMsnumberRequest timeout. Default 30000 (30 seconds).
headersobjectExtra headers on every request, for example your own x-request-id.
fetchfunctionA custom fetch implementation.

Example: booking to invoice

Lembah Bayu Travel Sdn. Bhd. confirms a group booking in its own system and wants the deposit invoice issued and emailed at once:

ts
import { Invoa, toSen } from "@invoa/api-sdk";

const invoa = new Invoa({ apiKey: process.env.INVOA_API_KEY! });

export async function onBookingConfirmed(booking: {
  ref: string;
  email: string;
  contactName: string;
  company: string;
  deposit: string; // "1250.00"
}) {
  // 1. Find the customer by email, or create them.
  const found = await invoa.contacts.list({ search: booking.email, limit: 1 });
  const contact =
    found.data[0] ??
    (await invoa.contacts.create({
      name: booking.contactName,
      companyName: booking.company,
      email: booking.email,
    }));

  // 2. Draft the invoice. The idempotency key makes retries safe.
  const draft = await invoa.documents.createDraft({
    type: "invoice",
    contactId: contact.id,
    reference: booking.ref,
    idempotencyKey: `booking-${booking.ref}-deposit`,
    lines: [
      {
        description: `Deposit for booking ${booking.ref}`,
        quantity: 1,
        unitPrice: toSen(booking.deposit),
        taxType: "SST_SERVICE",
      },
    ],
  });

  // 3. Issue it (a retried draft may already be issued), then email it.
  const invoice = draft.status === "draft" ? await invoa.documents.issue(draft.id) : draft;
  const { sent } = await invoa.documents.send(invoice.id);

  return { number: invoice.number, hostedUrl: invoice.hostedUrl, whatsappLink: sent.whatsappLink };
}

Methods

NamespaceMethodEndpointReturns
contactslist(query?)GET /contactsPage
get(id)GET /contacts/{id}Contact with balances
create(input)POST /contactsContact
update(id, patch)PATCH /contacts/{id}Contact
itemslist(query?)GET /itemsPage
create(input)POST /itemsItem
documentslist(query?)GET /documentsPage plus sums
get(id)GET /documents/{id}Document
createDraft(input)POST /documentsDocument
issue(id, options?)POST /documents/{id}/issueDocument
send(id, options?)POST /documents/{id}/send{ data, sent }
pdf(id)GET /documents/{id}/pdfArrayBuffer, or null while rendering
void(id, reason)POST /documents/{id}/voidDocument
paymentslist(query?)GET /paymentsPage
record(input)POST /payments{ payment, receipt, document }
scheduleslist(query?)GET /schedulesPage
get(id)GET /schedules/{id}Schedule
create(input)POST /schedulesSchedule
trigger(itemId, options?)POST /schedules/items/{id}/trigger{ item, issued, invoice }
einvoicesubmit(documentId)POST /einvoice/{documentId}/submit{ data, jobId, status }
status(documentId)GET /einvoice/{documentId}/statusStatus
webhookslist()GET /webhooks{ data, events }
get(id)GET /webhooks/{id}{ data, deliveries }
create(input)POST /webhooks{ data, secret }
update(id, patch)PATCH /webhooks/{id}Endpoint
delete(id)DELETE /webhooks/{id}nothing

Most methods return the API's data directly. List methods return the full page, { data, pagination }. Inputs and outputs use the same camelCase fields and integer sen as the API; see each resource page for the fields. documents.list takes status as an array and joins it for you.

documents.pdf returns null when the API answers 202 because the PDF is still rendering. Wait a few seconds and call it again.

Errors

Every non-2xx response throws an InvoaError:

ts
import { InvoaError } from "@invoa/api-sdk";

try {
  await invoa.documents.issue(draftId);
} catch (err) {
  if (err instanceof InvoaError && err.code === "plan_limit") {
    // err.status === 422, err.details has used / limit / plan
  }
  throw err;
}
PropertyDescription
statusHTTP status code.
codeThe API's error.code, for example validation or insufficient_scope.
messageThe API's error.message.
detailsThe API's error.details, if any.
requestIdThe x-request-id response header. Quote it when you contact support.

See Errors, limits and pagination for the codes.

Money helpers

FunctionExample
toSen(rm)toSen("1,250.00") returns 125000. Accepts a number or a string.
fromSen(sen)fromSen(125000) returns "1250.00".

Verify webhooks

verifyWebhookRequest checks the signature and timestamp (5 minutes tolerance by default) and returns the parsed event. It throws if the request is not genuine.

ts
import { verifyWebhookRequest } from "@invoa/api-sdk";

export async function POST(request: Request) {
  const rawBody = await request.text();
  let event;
  try {
    event = await verifyWebhookRequest(rawBody, request.headers, process.env.INVOA_WEBHOOK_SECRET!);
  } catch {
    return new Response("invalid signature", { status: 400 });
  }
  if (event.event === "payment.confirmed") {
    // event.data.paymentId, event.data.documentId, event.data.amount (sen)
  }
  return new Response("ok");
}

verifyWebhook(rawBody, signatureHeader, secret, timestampHeader, options?) does the same with the header values passed separately. Both accept { toleranceSeconds } as options. See Webhooks for the events and payloads.

Something unclear or out of date? Tell us at hello@invoa.io and we'll fix the page.