The door the rest of your systems come in through
You already run other software, and it needs to reach the records Structa keeps. That is three things: a read API over your own data, signed events pushed to you instead of polled for, and a way to let Structa act inside an account you already hold somewhere else. All three are running today, and where one stops, the limit is written beside it.
Eight resources, and nothing but GET
One version, one prefix: /api/v1/. Only GET is implemented, so every other verb on those paths answers 405 — there is no way to write into Structa through this API, and write endpoints are not shipped. Rows come back whole rather than trimmed: this is the business’s own data, behind a key its owner minted and can destroy.
| Endpoint | Query | Needs module |
|---|---|---|
GET /api/v1/bookings Both dates or neither. With a range, oldest first; without one, newest first. | ?from=YYYY-MM-DD&to=YYYY-MM-DD | Bookings |
GET /api/v1/clients Case-insensitive across name, email and phone. Ordered by name. | ?search= | Clients |
GET /api/v1/services The service catalogue a salon, spa or clinic books against. | — | Bookings |
GET /api/v1/menu Dishes and drinks with their prices. | — | Menu |
GET /api/v1/tables The floor, with each table’s live status. | — | Tables |
GET /api/v1/orders Line items ride along as items — an order without its lines is half an order. | ?day=YYYY-MM-DD | Orders |
GET /api/v1/payments Voided rows are included and marked. Exclude them before you add anything up. | ?day=YYYY-MM-DD | Payments & POS |
GET /api/v1/time-entries An open shift has a null clock_out. | ?from=YYYY-MM-DD&to=YYYY-MM-DD | Time clock |
curl -H "Authorization: Bearer sk_live_..." \
"https://app.structainc.com/api/v1/bookings?from=2026-09-01&to=2026-09-30"What comes back
- Every success is { data: [...], page: { limit, offset, total } }. limit defaults to 50 and stops at 200; offset is a plain row offset. There is no cursor.
- ?day, ?from and ?to are business-local calendar dates, resolved with the business’s own timezone and day-cutoff hour — a bar that closes at 04:00 keeps a 01:30 sale on the previous business day, exactly as its own reports do.
- Money is integer cents in *_cents columns. Timestamps are ISO 8601 UTC.
- Errors are { error, code }: BAD_REQUEST, INVALID_KEY, MODULE_DISABLED, NOT_FOUND, RATE_LIMITED, SERVER_ERROR, UNAVAILABLE. The code is the part to branch on.
- 120 requests a minute per key, plus a ceiling per IP address so a spray of invented keys is throttled too. Over budget answers 429 with Retry-After in seconds.
One business, shown once, revoked for good
A key is not an account and not a login. It is a bearer token minted by one business’s owner, scoped to that business, and destroyable by them without anyone’s help.
How one is issued
The owner opens the Open API module and creates a key with a label. It is sk_live_ followed by 64 hex characters, and it is shown exactly once, at that moment. Structa stores only its SHA-256 — a copy of our database cannot reproduce a working key. Minting is owner-only in the database itself, not by hiding a button: staff whose role carries the module see that keys exist, with the label, the first twelve characters, when it was made and when it was last used, and never the key.
What it can see
Exactly one business, and inside it only the modules that business has switched on. Two gates run on every request: the Open API module first, then the module the resource belongs to. Either one off and the answer is 403 MODULE_DISABLED, without saying which. Every query is pinned to the keyed business — there is no parameter that widens it and no cross-tenant surface to widen into.
How it is revoked
One control in the same list, behind a two-step confirm. Revocation is a timestamp and it is permanent — there is no un-revoke, you mint a new key. It is checked on the request, so the next call after it fails with 401. The list also shows last used, stamped by the API itself, which is how you tell a forgotten key from a live one before you kill it.
Switching the Open API module off stops every key for that business at once, and stops whatever events are still queued from going out. It is a kill switch as well as a setting.
Eight things Structa will tell you about
An endpoint is a URL of yours plus the topics you subscribe to. The event is captured the instant it happens and the queue drains every minute, so you are told rather than left to poll. Only the business owner may add, change or delete one, because an endpoint is a standing instruction about where the company’s data goes.
| Topic | Fires when | The payload carries |
|---|---|---|
| booking.created | a booking is written, by anyone or anything | its id, when it starts, the service, the status |
| booking.canceled | a booking’s status becomes canceled | its id, when it started, the status |
| client.created | a client is added | its id and the name — no phone, no email, on purpose |
| order.closed | an order’s status becomes closed | its id and when it closed |
| payment.recorded | a payment is taken | its id, the amount and tip in cents, the method, the order it belongs to |
| payment.refunded | a payment’s refunded total goes up | its id, the amount, and the refunded TOTAL — never the difference, because a partial refund can happen twice |
| request.created | a request is opened | its id, its priority, its status |
| delivery.shipped | a delivery’s status becomes shipped | its id, the tracking number, the carrier code, the order id at the shipping provider |
{
"id": "…", // the event
"topic": "booking.created",
"subject_id": "…", // the booking this is about
"occurred_at": "2026-09-12T09:12:00.000Z",
"delivery_id": "…", // stable across retries — dedupe on this
"attempt": 1,
"data": { "id": "…", "starts_at": "…", "service": "…", "status": "confirmed" }
}Events are thin deliberately. A key is held by someone who asked for it; a webhook URL is a line somebody typed into a form, and a mistyped host that receives a booking id has learned almost nothing, while one that received the row would have a customer’s name and phone. Call the read API with your key for the rest. A topic only fires while the business still has that topic’s module on — and the same question is asked again at delivery, so a module switched off while a retry was waiting stops that retry too.
Every delivery is signed, and the secret is shown once
A URL that accepts unsigned POSTs is a URL anyone can write to. Three headers arrive with every delivery, and the first one is the only thing you should believe the body on.
- Structa-Signature: t=<unix seconds>,v1=<hex hmac-sha256>. Also Structa-Topic and Structa-Delivery, which repeat two fields of the body so a router can dispatch without parsing.
- v1 is HMAC-SHA256 of `${t}.${rawBody}` — the raw bytes, before any JSON parsing — with your endpoint’s own secret. The timestamp is inside the MAC, which is what stops a captured delivery being replayable forever: reject anything more than a few minutes old in either direction, and compare in constant time.
- The secret is whsec_ followed by 64 hex characters, generated on our server and handed back exactly once — when you create the endpoint, or when you rotate it. Structa keeps only an encrypted copy and cannot show it to you again. Lost it, and the answer is to rotate, which mints a new one and shows that once.
- The test button sends a real POST to your URL and reports the status code it got back. It signs with a throwaway key rather than your secret, and says so in its own body: it proves the URL is reachable and takes the shape, not that your verification is correct. Claiming more would be a green light that means nothing.
- https only, enforced as a database constraint. Redirects are not followed — a 302 to an internal address is the oldest trick there is. Loopback, link-local and private-range addresses are refused. The honest limit: that check reads the address as written and does not resolve DNS, so a hostname that points at a private address still gets through; what bounds it is that only an owner can add or test an endpoint, and the answer they get back names one of four coarse failures rather than reading like a port scan.
// Structa-Signature: t=<unix seconds>,v1=<hex hmac-sha256>
const { t, v1 = "" } = Object.fromEntries(
header.split(",").map((p) => {
const i = p.indexOf("=");
return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
}),
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${raw}`) // the RAW body, before JSON.parse
.digest("hex");
const ok =
/^\d{1,10}$/.test(t ?? "") && // digits only, or the signed string is ambiguous
Math.abs(Date.now() / 1000 - Number(t)) < 300 &&
expected.length === v1.length && // timingSafeEqual THROWS on a length mismatch
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));Every attempt is written down
The queue drains every minute. What you get is the outcome of each attempt, never a green light computed from an endpoint merely existing — an endpoint nothing has been sent to says exactly that.
- At least once, not exactly once. A 2xx is success; anything else is retried. Dedupe on delivery_id, which is one value per event per endpoint and is stable across every retry of it.
- Eleven attempts, doubling from a minute and capped at six hours: about fourteen and a half hours from the first try to giving up, so a receiver that comes back the next morning still gets its backlog.
- A 4xx is not treated as permanent. A receiver answering 404 in the middle of a deploy is the ordinary case, and a permanent-looking status that was really transient would silently drop a real event.
- Each attempt records the status code, the error and how long it took, and the endpoint carries its last outcome and its consecutive-failure count. All of it is readable in the app, on the endpoint.
- Twenty failures in a row switch the endpoint off, with the reason recorded in the words the failure actually used. Turning it back on by hand clears the counter.
- Turning an endpoint off pauses it; it does not burn the queue. What was waiting goes out when you turn it back on. Anything still undelivered after twenty-four hours is given up on, and the row says which of the two it was.
- Two overlapping runs cannot both send the same delivery: a row is claimed before it is posted, and a claim left by a run that died goes stale rather than the row going missing.
- A POST is abandoned after ten seconds, and that counts as a failed attempt like any other.
Your account, your grant, revocable by you
There is no catalogue of connectors we wrote, and nothing is pre-wired. What exists is one door: the owner searches a provider’s app catalogue by name, signs in to the app there, and the grant is recorded against the business rather than against them.
Who may, and who may undo it
The owner, proved on the server for every action — including the read, because the list of what a business has connected is itself commercial information about how it runs. Granting needs the Open API module on and the business open. Revoking needs neither: an owner who switches the module off, or closes the business, must not thereby lose the only control that ends a live grant.
What Structa never holds
The sign-in and the refreshed token live at Pipedream, named in our data-processing agreement as a subprocessor for exactly this. Structa never receives or stores those credentials, and keeps no local copy of the connected list — it is read on demand, so it cannot go stale the moment you revoke access from the app’s own side. Disconnecting deletes the stored sign-in there, and erasing the business deletes every one of them; the app’s own settings remain the place to withdraw the permission at its source.
The grant belongs to the company
The connection is keyed to the business, not to whoever happened to click Connect. It survives that person leaving, and a person who works at two businesses can never carry one employer’s connected accounts into the other.
The one thing Structa reads by itself
A Gmail mailbox you name as the AI Receptionist’s, read where it already sits instead of forwarded to us. What it may read is bounded by a provider search you choose: the default is the inbox with Promotions and Social left out, and the database refuses to store an empty one, so no value of that field means “read everything”. Anything it takes in becomes a thread every colleague with that module can read, which is why the bound is a constraint rather than a promise in a prompt. Naming that mailbox needs the AI Receptionist module, not the Open API one.
That is the whole of it. We do not publish a number of supported apps, because the catalogue is the provider’s and a count on our page would be a claim about depth we have not built.
It proposes one thing, and a person signs it
Structa’s assistant can run an action from the provider’s own catalogue inside an account you connected — add a row, post a message, create an invoice. It is the only thing this product does whose effect lands where no migration of ours can undo it, so every part of it is deliberately narrow.
- It reads the action’s real input names from the catalogue first, and is forbidden from inventing either the action or its inputs. A guessed prop name is a call the vendor rejects and a person who was told it worked.
- It proposes exactly one action, with a sentence naming what will happen and in which app. That card is what a person confirms, and nothing runs before the confirm.
- The proposal is signed by our server to that business and to those exact arguments. Change a field after it was offered and it no longer validates. It expires in fifteen minutes, so a tab left open overnight cannot act on a business that has moved on.
- No batching, no retry, no decide-and-act. A retry belongs to whoever can tell an idempotent action from one that charges a card, and neither we nor the model can: “add a row” run twice is two rows.
- Every run is written to the business’s audit ledger before it is reported done — the app, the action, the summary and the inputs — whether it succeeded or failed. A write we cannot see is a write nobody can answer for, and this one happened in a system we do not own.
- If the app refuses, you get the app’s own words. Nothing here turns a vendor error into a cheerful summary.
- Only a manager or the owner, in a business that has both the AI Assistant and the Open API modules on. An employee without manager rights is never offered the tool at all.
- It costs 40 credits, charged whether the app accepted it or not, because the vendor call is made either way.
What this does not do
A buyer who finds the limit himself stops believing the rest of the page. So here they are, and every one of them is a rule in the code rather than a caution.
- There is no write API. v1 reads; a POST, PUT, PATCH or DELETE to any v1 path answers 405. Writes into Structa happen in the product, or through the assistant behind a confirmation.
- A key sees one business. There is no partner program, no app-install flow and no OAuth for third parties — a key is minted by a business owner, for their own business, and building on that owner’s data with their key is exactly what it is for.
- Pages are limit and offset, 200 rows at most, with no cursor. Deep paging over a large table is offsets, not a stream.
- Events carry ids and a handful of scalars, never rows. Anything richer is a call to the read API.
- Delivery is at least once. If double-processing an event would hurt, dedupe on delivery_id before you act on it.
- There is no sandbox and no test key. A test delivery is a real POST to your real URL, signed with a throwaway secret rather than yours.
- There is no SDK and no generated client. It is HTTP and JSON, and the snippet above is the whole of verifying a signature in whatever crypto library your language already ships.
- The private-address refusal reads the URL as written and does not resolve DNS.
- Nothing connects itself. An account has to be connected by the owner, and every action inside it has to be confirmed by a person on a card.
What you need before any of this works
- The Open API module switched on for the business. It gates the keys, the endpoints, the connected-accounts card and every /api/v1/ request.
- The owner, to mint a key or add an endpoint. Neither is delegable to a role, and both are enforced in the database rather than in the interface.
- Each resource’s own module on as well — bookings for bookings and services, orders for orders, payments for payments, down the table above.
- An https URL that answers quickly, for webhooks. Ten seconds is the whole budget, and a slow receiver reads as a failed attempt.
- Somewhere server-side to keep the key and the signing secret. Both are bearer credentials: treat them the way you treat a password, never in client code and never in a repository.
- For a connected account: the owner, signed in to that account through the catalogue search, once.
- For the mailbox: the AI Receptionist module, and a search that says what may be read.
What’s actually true today
- Eight read endpoints under /api/v1/, read-only — every other verb answers 405.
- A key is shown once, belongs to one business, and revoking it is permanent and immediate.
- Eight webhook topics, each signed HMAC-SHA256 with a per-endpoint secret that is also shown once.
- Every delivery attempt is recorded with its status code, its error and how long it took.
- A failure is retried eleven times over about fourteen and a half hours; twenty in a row switch the endpoint off with the reason attached.
- Turning an endpoint off pauses its queue instead of discarding it.
- An owner can connect an account they already hold, and Structa never receives that account’s credentials.
- Structa acts inside a connected account only on a card a manager confirmed, one action at a time, and every run lands in the audit ledger.
- Your data is yours: a full export any time, and permanent deletion on request.
Questions engineers ask
Is there a write API?
No. v1 is read-only and every non-GET verb on a v1 path answers 405. Nothing on this page describes a write endpoint, because none is shipped. What writes into Structa today is the product itself, and the assistant behind a confirmation card.
Can I build something other businesses install?
No. There is no partner program, no app directory and no OAuth for third parties. A key is minted by one business’s owner and sees only that business — which is the right shape for an agency or an in-house team building on a client’s own data with that client’s key, and the wrong shape for a marketplace app. We would rather say that plainly than imply otherwise.
Do you have a catalogue of integrations?
Not one of ours. The read API and the eight events are the general answer, and they are the same answer for a restaurant, a clinic and a reseller. Beyond those, an owner can connect an account they already hold through a provider’s catalogue, and Structa can run one action inside it when a person confirms it. Nothing is pre-wired, and we do not count connectors we have not built.
How do I know a delivery really came from you?
Recompute the MAC. HMAC-SHA256 over `${t}.${rawBody}` with your endpoint secret, where t is the value in the Structa-Signature header and the body is the raw bytes before parsing. Check the timestamp is within a few minutes in either direction, compare in constant time, and reject anything else. The code is above, and it is the same routine our own tests run.
What happens if my endpoint is down for a day?
The first eleven attempts spread over about fourteen and a half hours, so an outage you fix the next morning is covered. Past that the delivery is marked failed with the reason on it. Anything still undelivered after twenty-four hours is given up on, and after twenty consecutive failures the endpoint is switched off with the last error attached — turning it back on clears the count and releases what is still waiting.
What happens to the API if I turn a module off?
That module’s resource answers 403 MODULE_DISABLED, and its topic stops firing — the API obeys the same rule the rest of the product does, where data of a disabled module is invisible rather than merely hidden. Switching off the Open API module itself stops every key and every queued delivery for that business.
Do I need to turn anything on to start?
Yes: the Open API module in Owner setup, plus each resource’s own module. Which modules a business runs is its own choice — see the pricing page for what that costs.
Where is the reference?
This page is the honest summary. The endpoint list, the topics and a working curl example also sit inside the app, on the Open API module’s page next to your keys, so the reference is beside the thing it documents rather than in a manual that drifts.
Want the system these endpoints read from? See what else Structa runs →