Developers

API & webhooks

Last updated August 6, 2026

Read your Pombi data from your own tools, file leads back in, and get a signed POST the moment something happens. Version 1 is deliberately small: four read endpoints, one write, four events.

01Authentication

Create a key in Settings → Advanced. The key is shown once — Pombi stores only a SHA-256 hash of it, so it cannot be recovered later, only revoked and replaced. Each business can hold up to 10 live keys.

Send it as a bearer token on every request:

curl https://pombi.app/api/v1/bookings \
  -H "Authorization: Bearer pmb_<business>.<key-id>.<secret>"

A key names its own business, so there is no account parameter and a key can only ever read the business it was minted for. Anything wrong with the key — unknown, revoked, mistyped, or belonging to someone else — returns the same flat 401, with no detail that would let the key space be probed. A lapsed subscription returns 402.

02Rate limits

Each key is capped at 60 requests per minute, counted across every endpoint. Over the cap you get a 429 with a Retry-After header in seconds. The budget is shared across all Pombi instances, so the number is the real one.

03Endpoints

Read endpoints accept ?limit= (default 50, maximum 200). This is the complete list — there are no undocumented endpoints.

  • GET /api/v1/customers

    Your customer list, newest activity first.

  • GET /api/v1/bookings

    Bookings, newest first.

  • GET /api/v1/calls

    Conversations (phone, SMS, web chat) with their pipeline stage.

  • GET /api/v1/invoices

    Invoices from your connected Stripe account.

  • POST /api/v1/leads

    File a new lead. Creates the same records the '+ Add lead' button does.

GET /api/v1/bookings also takes ?status=, and GET /api/v1/calls takes ?stage=. Call transcripts are not exposed — the API returns conversation metadata, not what your customer said.

Invoices are a live read from Stripe. If no Stripe account is connected, the response is {"configured": false, "invoices": null} — not an empty list. “You have no invoices” and “we cannot see your invoices” are different facts and we will never merge them.

Records created by the in-app Live tester carry "simulated": true. They are practice runs, not people — do not count them.

curl -X POST https://pombi.app/api/v1/leads \
  -H "Authorization: Bearer $POMBI_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"Dana Reyes","phone":"555-0148","service":"Deck rebuild","note":"Found us on Google"}'

# 201 {"ok":true,"callId":"call_…","bookingId":"bk_…"}

04Webhooks

Register an https endpoint in Settings → Advanced (up to 5 per business) and pick the events you want. Pombi POSTs JSON:

{
  "id": "whd_…",              // delivery id — stable across the retry
  "event": "booking.created",
  "at": "2026-08-06T14:02:11.004Z",
  "businessId": "biz_…",
  "data": { … }               // event-specific
}

Delivery is best-effort with exactly one retry a second after a failure, using the same delivery id and the same signature. De-duplicate on pombi-delivery-id. The event name also arrives in pombi-event. Answer with any 2xx; anything else (or no answer within 4 seconds) counts as a failure and is shown to the business owner verbatim on their settings card, including the status code.

A webhook never affects the thing that triggered it. If your endpoint is down when a customer books, the booking is still saved and the customer still gets their confirmation.

05Events

Four events, and an honest note on each about where it fires. Where coverage is partial, it says so — please read these before you build a sync on one.

  • lead.created

    A new lead came in.

    A submission to one of your lead forms, or POST /api/v1/leads.

  • booking.created

    A booking was captured.

    Bookings captured on the inbound rails: your website storefront, a lead form, a phone/chat conversation, and POST /api/v1/leads. Bookings you type into the dashboard yourself do NOT emit this event yet.

  • booking.completed

    A booking was marked completed.

    Every path that marks a booking completed, including the dashboard — this one has no gaps.

  • invoice.paid

    A pay link settled (Stripe-verified).

    A text-to-pay / invoice pay link that Stripe confirmed as paid on your connected account. Invoices paid directly inside Stripe are not observed here.

06Verifying the signature

Every delivery carries a pombi-signature header in the same shape Stripe and Svix use:

pombi-signature: t=1754487731,v1=6f1a…c2

v1 is a hex HMAC-SHA256 over the exact string `${t}.${rawBody}`, keyed with your endpoint’s signing secret (shown once when you register the endpoint). Compute it over the raw request body, before any JSON parsing, and compare in constant time. Reject anything where t is more than 300 seconds from your own clock — that is the replay guard.

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

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=")),
  );
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

If you rotate a secret, delete the endpoint and register it again — we cannot show you an existing secret twice.

07What v1 doesn't do

No write access beyond POST /api/v1/leads. No pagination cursors (limit only). No sandbox environment. No way to read call transcripts. If you need one of these, get in touch — we would rather hear the real use case than guess at it.