Toolaby

Sync customers

Keep a table of who holds your tool on your own server, with webhooks and the API.

Your server may need to know who holds a tool: to unlock a web app the extension pairs with, to send onboarding email, or to reconcile with a CRM. This guide keeps a customers table current with webhooks, and corrects it with the API.

Before you begin

  • An API key from Configure → API, and the API's address shown there: https://<workspace>.toolaby.app/api/v1 on Live, your workspace's Test address with /test/api/v1 on Test.
  • A server that can receive HTTPS requests. For development, npx -y toolaby@latest webhooks trigger reaches a server on your machine; see Webhooks.

1. Load the table

Page through the tool's customers once and store each.

const base = 'https://northwind.toolaby.app/api/v1'; // on Test: https://quiet-otter-4172.toolaby.app/test/api/v1
let cursor = null;
do {
  const url = new URL(`${base}/tools/${tool}/customers`);
  url.searchParams.set('limit', '100');
  if (cursor) url.searchParams.set('cursor', cursor);
  const page = await (await fetch(url, { headers: { Authorization: `Bearer ${key}` } })).json();
  for (const c of page.data) await db.customers.upsert({ email: c.email, entitled: c.entitled, via: c.via });
  cursor = page.next_cursor;
} while (cursor);

2. Keep it current with webhooks

Register your endpoint, with every event: npx -y toolaby@latest webhooks add https://your-server.example/webhooks/toolaby --env-file .env registers it and writes its signing secret into .env as TOOLABY_WEBHOOK_SECRET (or Configure → Webhooks in the dashboard). A coding agent does it on Test as part of the job, with the toolaby_add_webhook tool. Until it is registered, no real event reaches your server. On each delivery, verify the signature, then apply the change:

EventChange
purchase.completedentitled: true, via from kind (licence or subscription), for the row of its email. A licence bought signed out (account: 'none') has no account yet: its email address is the only way to match it.
subscription.startedentitled: true, via: 'subscription', for the row of its email.
subscription.cancelledFor the row of its email: with immediately: true, entitled: false. Otherwise access continues until ends_at: keep entitled: true and record the date.
payment.refundedentitled: false for that licence.
payment.disputedFor a licence: entitled: false while the dispute is open. With licence empty, the payment was a subscription's, which runs on: nothing changes.
dispute.closedFor a licence, won: entitled: true; lost: entitled: false. An inquiry that closes without a chargeback sends no event: step 3 corrects it.

Store the event id and ignore a delivery whose id you have seen. Answer 2xx before writing to the database.

Grants and trials made in the dashboard or by the API are not events, and neither is a subscription's failed payment, which unlocks nothing while it is past due. Step 3 covers them. See Subscriptions.

3. Correct with the API

Webhooks tell you what changed; the API tells you what is. Once a day, or before an action that matters, retrieve the customer:

const c = await (await fetch(`${base}/tools/${tool}/customers/${encodeURIComponent(email)}`, {
  headers: { Authorization: `Bearer ${key}` },
})).json();
await db.customers.upsert({ email, entitled: c.entitled, via: c.via });

entitled is the truth at that moment, including grants, trials and bundles. A workspace makes 20,000 lookups a day; to correct more customers than that, page through the list as in step 1.

Notes

  • A team licence entitles the seat holders, not the buyer; each holder appears as a customer with via: 'licence'.
  • A buyer who changes their address keeps their account; the API answers for the new address.
  • Deliveries can arrive out of order. When two events concern the same subscription, the later created wins.

On this page