Toolaby Wall
Reference

API

Look up whether a person holds a tool, and grant or revoke access, from your own server.

The API is REST over HTTPS, at your workspace's address. Requests carry an API key as a bearer token; bodies and responses are JSON. The OpenAPI document is at /api/v1/openapi.json, and a generated reference to read at toolaby.app/api/v1/docs.

https://<workspace>.toolaby.app/api/v1

Authentication

Create a key under Configure → API in the dashboard. It is shown once. Test keys (wk_test_…) act on the test Wall's data; live keys (wk_live_…) on the live Wall's.

curl https://northwind.toolaby.app/api/v1/tools/invoice-grabber/customers/maya@example.com \
  -H "Authorization: Bearer wk_live_…"
const res = await fetch('https://northwind.toolaby.app/api/v1/tools/invoice-grabber/customers/maya@example.com', {
  headers: { Authorization: `Bearer ${process.env.WALL_API_KEY}` },
});
const customer = await res.json();

There is no SDK; the API is five endpoints, called with fetch. A key acts only on tools in its workspace. A tool in another workspace, or one that does not exist, answers 404.

Rate limits

600 requests per minute per key. Above that, 429 with rate_limit_error.

  • List customers: 30 pages a minute per workspace, all its keys together. Each page reads the tool's whole list, and the rows it reads count toward the workspace's 300,000 a day, the dashboard's Customers page with them: ask for 500 a page to read a long list in few. See Limits.
  • Retrieve a customer: 20,000 a day per workspace, all its keys together, and 20,000 across all the workspaces of its owner, counted before it reads. List grants is a lookup by address too, counted with it. The day is the 24 hours from the first lookup in it. To know every customer, page through the list; to keep up, use webhooks: see Sync customers.
  • Create a grant: 1,000 a day per tool, through the API and the command line together. Each grant is also one of the workspace's 20,000 additions a day, and of its owner's 20,000 across their workspaces, counted before it is written: see Limits. Bring more in with the tool's Import page.

Errors

Every error has the shape:

{ "error": { "type": "invalid_request_error", "message": "days must be between 0 and 3650", "param": "days" } }
Statustype
400invalid_request_errorA parameter is missing or wrong; param names it.
401authentication_errorNo key, a malformed key, a revoked one, or one whose workspace's owners are suspended.
403plan_limit_errorThe workspace's plan does not allow it. On Hobby a workspace holds 100 passes at once: Plans.
404not_foundNo such tool, grant or person in this workspace.
429rate_limit_errorThe key's minute is used up, this address sent thirty wrong keys in a minute, the workspace read 30 pages of customers this minute or its lists' rows for today (or its owner's, across their workspaces), it looked up 20,000 customers today (or its owner did), the tool made 1,000 grants today, or the workspace made its 20,000 additions today.
500api_errorThe Wall's fault, or it cannot count the workspace's additions just now. Retry.

Endpoints

List customers

GET /tools/{tool}/customers

Everyone who holds the tool or has signed in to it, newest first: the newest 2,000 of each kind, as in the dashboard. A customer beyond them answers Retrieve a customer.

Parameter
toolpathThe tool id.
limitquery, optional1 to 500. Default 50.
cursorquery, optionalnext_cursor from the previous page.
entitledquery, optionaltrue: only customers who hold the tool now; false: only those who do not.

Returns a page.

{
  "data": [
    { "email": "maya@example.com", "account": true, "entitled": true, "via": "subscription", "since": "2026-09-18T13:33:55.000Z", "last_active_at": "2026-09-20T08:12:01.000Z", "devices": 2 }
  ],
  "has_more": true,
  "next_cursor": "NTA"
}
let cursor = null;
do {
  const url = new URL('https://northwind.toolaby.app/api/v1/tools/invoice-grabber/customers?limit=500');
  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 upsertCustomer(c);
  cursor = page.next_cursor;
} while (cursor);

Retrieve a customer

GET /tools/{tool}/customers/{email}

Whether the address holds the tool now, and how. An address with no account still answers for a licence bought under it. Each lookup is one of the workspace's 20,000 a day: see Rate limits.

Parameter
toolpathThe tool id.
emailpathThe address, URL-encoded. Another spelling of a Gmail address, with or without dots, finds the same account.

Returns a Customer.

{
  "email": "maya@example.com",
  "account": true,
  "entitled": true,
  "via": "subscription",
  "paid_at": "2026-09-18T09:38:35.000Z",
  "plan": { "id": "yearly", "name": "Yearly", "billing": "subscription", "amountCents": 2900, "currency": "eur", "interval": "year" },
  "subscription": { "status": "active", "periodEnd": "2027-09-18T09:38:35.000Z", "cancelAtPeriodEnd": false, "cancelAt": null },
  "trial": null
}

List grants

GET /tools/{tool}/grants?email={email}

The address's grants and trials for the tool, newest first.

Parameter
toolpathThe tool id.
emailqueryThe address.

Returns { "data": [Grant, …] }.

Create a grant

POST /tools/{tool}/grants

Grants access without payment. An unknown address gets an account; the grant applies when they sign in. The extension unlocks at its next check.

Parameter
emailstring, requiredThe address, at most 254 characters.
daysinteger, optional1 to 3650. Absent or 0: until revoked.
notestring, optionalUp to 200 characters, shown in the dashboard. A NUL character is refused.
curl -X POST https://northwind.toolaby.app/api/v1/tools/invoice-grabber/grants \
  -H "Authorization: Bearer wk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "email": "maya@example.com", "days": 30, "note": "Beta tester" }'
const grant = await (await fetch('https://northwind.toolaby.app/api/v1/tools/invoice-grabber/grants', {
  method: 'POST',
  headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'maya@example.com', days: 30, note: 'Beta tester' }),
})).json();

Returns the Grant with status 201.

Revoke a grant

DELETE /tools/{tool}/grants/{id}

Ends the grant. The extension stops at its next check, within 15 minutes. Revoking twice succeeds.

Returns { "id", "tool", "revoked_at", "live": false }.

Objects

CustomerListItem

AttributeType
emailstring
accountbooleanThe address has signed in to your workspace.
entitledbooleanHolds the tool now.
viaenum, nullablelicence, subscription, trial, grant, or null.
sincestringWhen they first held the tool, or first signed in to it.
last_active_atstring, nullableThe last check-in from any of their devices.
devicesintegerDevices signed in or activated.

Customer

AttributeType
emailstring
accountbooleanThe address has signed in to your workspace.
entitledbooleanHolds the tool now: by a licence, subscription, trial, grant, or a bundle that names it.
viaenum, nullablelicence, subscription, trial, grant. null when not entitled.
paid_atstring, nullableThe licence's purchase date, or the current period's start.
planobject, nullableid, name, billing (one_time or subscription), amountCents (nullable), currency, interval (month, year or null).
subscriptionobject, nullablestatus (Stripe's), periodEnd, cancelAtPeriodEnd, cancelAt. Present whatever the status, so a lapsed subscription can be explained.
trialobject, nullableendsAt, active. Present after the trial ended, with active: false.

Grant

AttributeType
idstring
toolstringThe tool id.
emailstring
kindenumgrant or trial.
starts_atstring
ends_atstring, nullableWhen the grant ends by itself. null for a grant that lasts until revoked.
revoked_atstring, nullable
notestring, nullable
created_atstring
livebooleanEntitles the address now.

Dates are ISO 8601 strings.

On this page