The PicksYou API
Everything on this page is generated from the same contract our own tests hold the code against, so it cannot quietly stop being true. It is a server to server API: a key belongs on your server, never in anything a visitor can read.
What this is
What AI assistants say when somebody asks for a business like this one.
The paragraphs below are the contract's own preamble, printed word for word. They are the terms a client inherits by calling this API at all.
A client reads, for one business, how often assistants picked it out of how many answers, the questions and the answers word for word, what is standing in the way of being picked, the visits that arrived at the business's own site from an assistant, and the snippet that records them. It can start a check, and a distribution partner can provision a shop, put it on a plan, retire it and erase it.
THE LAW A CLIENT INHERITS. Every count in this API travels with the n it came out of, the interval around it and the sentence we would print ourselves, and a client prints all of it or none of it. There is no key anywhere in this contract for a place in a list, a grade or a share of anything, and inventing one is publishing a claim the data does not carry. docs/api/README.md carries the worked example and the words to avoid.
EVERY ANSWER, INCLUDING EVERY REFUSAL, carries Cache-Control no-store and an X-Request-Id (a ULID) to quote when you write to us. There are no exceptions: an address nothing answers and a verb an address does not take are refused by the router before the group runs, and the error renderer mints the id for them.
THIS API IS SERVER TO SERVER. No Access-Control header is ever sent, so a browser page cannot call it cross-origin, and a key belongs on your server rather than in anything a visitor can read.
Where to send it
Two addresses, each with its own database, its own keys and its own receipts. A key minted on one never answers on the other.
| Base address | Which one it is |
|---|---|
| https://picksyou.com/api/v1 | Production. |
| https://staging.picksyou.com/api/v1 | Staging. Its own database, its own keys, its own receipts. |
The key, and how to send it
Every request carries one key in the Authorization header. There is no other way in: no cookie, no session, no query parameter, and no browser page can call this API cross origin because no Access-Control header is ever sent.
Authorization: Bearer
| Kind | How it is sent | What it is |
|---|---|---|
| customerKey | http bearer | A customer's own key, py_c_ and then 48 characters. The account holder mints it at /app/account/api, chooses whether it may start checks, and may scope it to one business; it is shown once and stored as a hash, so a lost key is revoked and replaced rather than recovered. Send it as Authorization: Bearer py_c_... |
| partnerKey | http bearer | A distribution partner's key, py_p_ and then 48 characters, issued by us in the admin panel and shown once. It carries all three abilities and it is the only kind of key the partner endpoints answer. Send it as Authorization: Bearer py_p_... |
What a key may do
A key carries one ability and the read ability comes with all of them. An endpoint that asks for an ability a key does not carry is refused, and minting a new key is the only way to widen one.
| Ability | What it opens |
|---|---|
| read | Every GET on this page: the business, the weeks, the window, the runs, the answers, the blockers, the names beside you, the receipts and the snippet. Every key carries it. |
| checks | Starting a check, which spends real money with several vendors and is capped per day. A customer chooses it when they mint the key; every partner key carries it. |
| connections | A distribution partner's own doors: provisioning a shop, putting it on a plan, retiring it and erasing it. Only we can issue a key that carries this. |
| pages | The two doors a customer's own website opens for the pages that customer approved. Nothing reaches either list without an approval the customer wrote first. |
| partner_links | The two doors a customer's own website opens for its partners page. Every pair on that page was agreed by both businesses, one click each. |
The limits, and where each one bites
Counted per key and per address, in the buckets below. They are written on the groups rather than on each endpoint, because a bucket is shared by every door in its group.
- key
- Who this key is and what it may see. Rate limit: 120 requests a minute for a customer key and 600 for a partner key, counted on the KEY. On top of that, 600 requests a minute from one address, refusals included, and 60 refused requests a minute from one address in a bucket of their own. The address caps are generous because one shared host carries hundreds of sites; the per key cap is the one that bites.
- reads
- The panel's own numbers, read by a machine: the business, the weeks, the window cut by assistant or by question, the runs, the answers, the blockers, the names beside you, the receipts and the snippet. Every one of them is a GET and needs the read ability, which every key carries. Rate limit: the per key cap of the key tag.
- checks
- Starting a check. Needs the checks ability, which a customer chooses when they mint the key and every partner key carries. Rate limit: 20 a minute per key, over a day cap of 3 checks that is the one that decides. The day cap is counted PER KEY on a customer key and PER SHOP on a partner key, because one partner key serves every merchant that installed the app and the fourth shop to install on a given day must still get its first check. A check spends real money with five vendors, which is why the day cap is small; a repeat inside the dedupe window is answered with the run that already exists rather than a second one.
- pages
- The two doors a customer's own website opens for the pages that customer approved. Needs the pages ability, which a customer ticks when they mint the key on the WordPress page and which a partner key can never carry. Rate limit: 20 a minute per key, in a bucket of their own, so a site syncing its pages cannot shut its own checks door. The site PULLS: it fetches what its owner already approved and tells us what it did, and we hold no credential of the site and nothing of it but the id of the post it made. Nothing appears on either list without an approval the customer wrote first, and what actually writes the receipt is our own read of the published page, never the report.
- partner links
- The two doors a customer's own website opens for its partners page. Needs the partner_links ability, which a customer ticks when they mint the key on the WordPress page and which a partner key can never carry. Rate limit: 20 a minute per key, in a bucket of their own, so a site syncing its partners cannot shut its own pages or checks door. A partners page lists the businesses in other trades this business recommends and that recommend it back; every pair on it was agreed by both businesses, one click each, and each click wrote an immutable approvals row. The site PULLS the list on its own clock and prints what it last read, and the one thing it tells us about itself is which page shows the list.
- partner
- A distribution partner's own six doors. Needs a partner key with the connections ability, which only we can issue. Rate limit: 600 a minute per key, plus the two address caps.
When something is refused
One shape for every refusal, so a client writes one branch and reads the code. The message is English on every request and is for a person reading a log, never for a business owner reading a page.
| Field | Type | What it means |
|---|---|---|
| error | object | |
| error.code | string | The closed list. Adding one is a change to this file as well as to the code, and a test holds the two together. |
| error.message | string | One English sentence. Never a trace and never a file path. |
| error.fields | object | On validation_failed only: the field name that could not be used, and what was wrong with it. |
| error.retry_after | integer | On rate_limited, checks_limit_reached, api_paused and plan_grants_paused: seconds to wait. The same number is in the Retry-After header. |
| error.limit | integer | On checks_limit_reached only: how many new checks a day this key may start (per key for a customer key, per shop for a partner key). A client prints this number; it never parses the message for it. |
| error.paused | object | On business_paused only (402). This business has an open invoice, so its dashboard and every read about it are paused until the sum is paid. Nothing has been removed. The facts are the ones the panel's own paused page states, so a plugin can print a sentence its owner can act on: the sum, what it is made of, and where it is settled. |
| error.paused.amount | string | The open sum, as a decimal string. |
| error.paused.currency | string | |
| error.paused.vat | string | How much of the sum is VAT, as a decimal string. |
| error.paused.invoice_count | integer | |
| error.paused.invoices | array of object | Every open invoice, oldest first, by number, issue date and gross. They are what the sum adds up to. |
| error.paused.paused_since | string | The day the dashboard shut, or null. |
| error.paused.plan_ends_on | string | The day the plan ends if the sum is still open then, or null. |
| error.paused.billing_url | string | The page that settles it. |
The closed list of codes
Adding one is a change to the contract as well as to the code, and a test holds the two together.
- unauthenticated
- forbidden
- not_found
- method_not_allowed
- validation_failed
- rate_limited
- checks_limit_reached
- business_owned
- business_connected
- connection_not_active
- api_paused
- business_paused
- plan_grants_paused
- plan_required
- body_too_large
- server_error
What each refusal means
- Unauthenticated
- No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one.
- Forbidden
- The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one.
- NotFound
- There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have.
- MethodNotAllowed
- That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way.
- ValidationFailed
- Something you sent could not be used, and error.fields names it. Nothing is ever silently clamped: limit=5000 is a refusal rather than a page of fifty, because a client that asked for five thousand and was handed fifty without being told has written a loop that never terminates.
- BodyTooLarge
- The request body is over 16 KB, refused in front of the controller.
- RateLimited
- Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key.
- ServerError
- Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us.
- ApiPaused
- The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything.
Every endpoint
One section per address and verb, in the contract's own order. Every answer also carries a request id to quote if you write to us, and is never stored in a shared cache.
GET /me
What this key is and what it can see
The first call an integration makes. A key pasted into a settings field has to answer three questions before anything is drawn: did it work, whose is it, and which business is it for. A key minted for one site lists that one site, so a plugin installed on one site never has to show a chooser. Never in this body: the token, its hash, or the account's address.
What goes in the address
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | Me | The key, its actor, and what it may read. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses
Every business this key may read
A customer key lists the account's own businesses, narrowed to one when the key carries a scope. A partner key lists the shops it has a live connection to; an erased shop is gone from here, because a redaction is a merchant asking to be forgotten and a list that still named them would be the one place the name came back. Ordered by id, always. This list is not paginated: an account holds at most five businesses, and a partner with many shops walks /partner/connections, which has a cursor.
What goes in the address
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | BusinessList | The businesses, in id order. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}
One business and where it stands
The call to make on every page load: what the business is, the one count that is the headline, whether anything is running right now, and when it was last checked. The headline is the rolling window over the newest four ISO weeks of our own sampling, or the free check when that is all there is, or nothing at all. Nothing at all is a null headline and the word none, never a zero: no sample means no information.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | BusinessSummary | The business and its summary. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/weeks
The weekly ledger and the rolling window
Two lists, because they answer two questions: weeks is what each week's own sample said, rolling is what the last four weeks said together, and the second is where every headline is read from. A free check appears in weeks with kind free_check and never in rolling: it was a different size, so it neither widens nor narrows the usual range. Oldest first.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
| weeks | query | No | How many of the newest weeks to return. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | WeekLedger | The ledger, oldest first. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 422 | Error | Something you sent could not be used, and error.fields names it. Nothing is ever silently clamped: limit=5000 is a refusal rather than a page of fifty, because a client that asked for five thousand and was handed fifty without being told has written a loop that never terminates. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/runs
The runs of this business, newest first
Cursor paginated on the id. Pass the previous page's next_before to get the next page; next_before is null on the last one. An offset would repeat or skip a row whenever a check finished between two calls, which on a list that grows at the front is the normal case rather than an edge one.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
| limit | query | No | Runs per page. |
| before | query | No | The id the previous page ended on. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | RunPage | One page of runs, newest first. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 422 | Error | Something you sent could not be used, and error.fields names it. Nothing is ever silently clamped: limit=5000 is a refusal rather than a page of fifty, because a client that asked for five thousand and was handed fifty without being told has written a loop that never terminates. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/runs/{run}
One run, its questions and every answer word for word
The whole of one check: the run with its counts, the questions it asked as it asked them, and every answer with the verdict the panel gave it. THIS IS THE ENDPOINT A CLIENT POLLS while a check is running, every couple of seconds, and it carries an ETag for exactly that: send the tag back as If-None-Match and an unchanged run answers 304 with no body. The tag moves when the status changes, when an answer lands, and when a person reviews an appearance, because a review turns a possible into a picked without any new answer arriving. A run of another business under this business's id is a 404.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
| run | path | Yes | The run, in either spelling: the integer id, or the opaque public_id the same payloads carry. |
| If-None-Match | header | No | The ETag of the copy you already have. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | RunDetail | The run, its questions and its answers. |
| 304 | Nothing has changed since the tag you sent. No body. Keep polling with the same tag. | |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/answers
The answers themselves, word for word
Not a claim and not a summary: the assistant's own paragraph, with the business's name in it or without it. run=latest is the run the panel itself reads, which is the newest sampled run when a sampled week exists and the newest finished free check when none does. A business with nothing to show answers 200 with a null run_id and an empty list, never a 404: nothing checked yet is a state to draw, not an error to report.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
| run | query | No | The word latest, or a run in either spelling: the integer id, or the opaque public_id the same payloads carry. |
| named | query | No | all is every verdict in the run. yes is the verdict picked. no is the verdict not_picked, and nothing else: an answer that never arrived and an empty AI Overview did not fail to name anybody, so they are in neither list. |
| limit | query | No | Answers per call. A Growth week is 275 of them. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | AnswerPage | The run's answers, oldest first. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 422 | Error | Something you sent could not be used, and error.fields names it. Nothing is ever silently clamped: limit=5000 is a refusal rather than a page of fifty, because a client that asked for five thousand and was handed fifty without being told has written a loop that never terminates. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/blockers
What is in the way, in the order an owner should read it
The task list a client turns into a panel on somebody's own dashboard. The order is the product: failures before warnings, then the weight the research gives each check, and a client that sorted them itself would be telling a business owner to fix the cheap thing first. blockers are the tasks; rest is everything else we looked at and found nothing wrong with. A run whose checks have not run is two empty lists and a null checked_at, never a clean bill of health. Nothing here publishes anything anywhere: every fix is the owner's own act on their own site.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | BlockerReport | The blockers and the rest of the readings. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/named
The businesses AI named beside this one
Growth only. A Starter, a Free business and a business on no plan all answer 404, exactly as the panel does: this product does not sell by showing somebody a door they cannot open, and a 404 also tells a stranger nothing about which plan a business is on. Every row is a name and how many answers named it, this week against four weeks ago. The rows arrive with the biggest count first because a list has to be in some order; the order is not a claim and there is no key here that says it is.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | NamedLedger | The names and their counts. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/receipts
People who arrived from an assistant
The other half of the proof: the weeks say how often assistants named the business, this says how many people then arrived at the business's own site from one. A count of arrivals and nothing else, over a fixed window of 30 days, with every day printed including the empty ones. There is no address, no user agent, no cookie and no identifier in any of it, because the snippet never wrote one.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | Receipts | The visits, by assistant and by day. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/snippet
The receipts snippet for this business
The one step of this product that asks a business owner to touch their own website, handed to a client so that they never have to. The site key, the two URLs built out of this deployment's own routes, whether we are recording at all right now, and one sentence in the owner's language to print beside the switch. The URLs are built rather than written down, so a client pointed at staging never posts a live shop's visits into a test database.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | Snippet | The site key, the two URLs and the instruction. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/slices
The window cut by assistant or by question
The same window the weeks endpoint counts, cut one way or the other: one row per assistant, which is the panel's matrix card, or one row per question, which is the panel's questions page. Every row carries the n it was measured over, the usual range as whole counts of that same n, and the sentence we would print ourselves about how much weight the row will bear, because a cut of a window is small enough that some of its rows cannot be read on their own. A week nobody asked that assistant or that question in says asked false and counts in no total: it is not a zero, since nobody asked. There is one range per row and never one per week, and there is no difference between two weeks anywhere in it.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
| dimension | query | No | Which way to cut the window. |
| weeks | query | No | How many of the newest weeks the window covers, as the panel's own range control offers them. 0 is every week we hold. Anything else is refused rather than corrected. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | SliceReport | The cut, one row per assistant or per question. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 422 | Error | Something you sent could not be used, and error.fields names it. Nothing is ever silently clamped: limit=5000 is a refusal rather than a page of fifty, because a client that asked for five thousand and was handed fifty without being told has written a loop that never terminates. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/reports
The months that have closed, with their frozen figures
One row a closed month, newest first: the count of answers that named the business that month, the n it came out of, and the two ends of the usual range as whole counts of that same n. Every figure was measured once, on the day the month closed, and none of them ever moves again except by an amendment, which is stamped on the row. A month still running is NOT in this list: it has no frozen figures behind it, and a row that changed every Monday under the name of a month would be the one thing a report may never be. For the live measurement, read the weeks and the slices, which say out loud that they are live.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | ReportList | The closed months, newest first. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/reports/{period}
One closed month, exactly as it was frozen
The whole document the customer holds on paper: the month's count and its usual range, every sampled Monday with its own count and its cut by assistant, what was in the way and what got fixed, who arrived from an assistant, and the two answers the report quotes. It is read off the stored document and nothing is counted again, so a match a person confirms in November does not move a September an agency has already forwarded. A month that never closed, and a month of another business, are the same 404. The PDF itself is not served over this API in v1.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
| period | path | Yes | The month, as four digits, a hyphen and two. It is the spelling the report's own filename carries and the one the archive links, so a client stores one form of a month and no other. A segment that is not that shape never reaches a controller. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | MonthReport | The frozen document. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
POST /businesses/{id}/checks
Start a check on this business
Needs the checks ability. The money rules are the free checker's own, never a softer copy: a fresh run of any kind inside the dedupe window is reused rather than bought again, a run that failed is retried and keeps the answers it already collected, and a day whose budget is committed dates the run for tomorrow and answers queued. There is no request body: the business is in the path, and a body over 16 KB is refused with body_too_large before the controller runs. Poll run.poll_url until the run's status is done or failed. run.receipt_url is a public results page a client may open for the owner. A partner key may only start a check on a shop that is INSTALLED: an uninstalled shop stays readable, because its history is why the row was kept, and answers 409 connection_not_active here.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | CheckAccepted | reused: a fresh run already existed and you were handed that one. Nothing was created and nothing was spent. |
| 202 | CheckAccepted | started, retried or queued. The run exists and is on its way; poll it. queued means today's budget is committed and the run is dated for tomorrow morning. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 409 | Error | connection_not_active: a partner key asked for a check on a shop that is not installed. Nothing was written and nothing was spent. Reading that shop is still allowed. |
| 413 | Error | The request body is over 16 KB, refused in front of the controller. |
| 429 | Error | rate_limited (20 a minute per key) or checks_limit_reached (the day cap of 3, per key on a customer key and per shop on a partner key). Both carry retry_after in the body and a Retry-After header. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/pages/queue
The pages this site may put up, and take down
Needs the pages ability. Two lists, and both of them are empty until a customer has approved something. publish is every page this business approved on the plugin route whose approvals row exists, is not an undo and has not been reverted; html is the text of that approvals row itself, which is the body with the disclosure line as its last paragraph and the one copy of it that can never be edited. unpublish is every page whose owner pressed Undo and which this site has not reported taking down yet, with the approvals row of the undo and the post id you told us about. Ordered by our own id, so two syncs agree and a half finished one can be repeated. Never cached: the body is one customer's own prose.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | PagesQueue | The two lists, either or both of them possibly empty. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
POST /businesses/{id}/pages/{page}/report
Tell us what this site did with a page
Needs the pages ability. Report one item off the queue: the action you took, whether it worked, the post id you made and the address it is at. The whole body is appended to the customer's own approvals row word for word, so they can read what their site said. On a done publish the page is marked as landed once and we then go and read that address back ourselves, and only that read writes the receipt. On a done unpublish the page is marked as taken down and the undo row is read back the same way. On failed nothing moves and the item stays on the queue, so the next sync tries again by itself. Reporting the same thing twice is safe: the second report is recorded and changes nothing. A page that is not this business's, or is not waiting for the action you named, is a 404 and writes nothing at all. The address has to be an https one on the business's own website, because it is what we print to them and what we are about to fetch. A body over 16 KB is refused with body_too_large before the controller runs.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
| page | path | Yes | The page, as the queue named it: the opaque public_id, fourteen characters starting on a letter. There is no second spelling, because the queue is the only place a client ever learns of a page and it hands out this one. A page that is not this business's answers the same 404 as one that does not exist. |
What you send
| Field | Type | Required | What it means |
|---|---|---|---|
| action | string | Yes | Which of the two lists this item came off. |
| outcome | string | Yes | done means the site did it. failed means it could not, nothing moves, and the item stays on the queue for the next sync. |
| post_id | integer or null | No | The id of the post you made, or the one you updated. |
| url | string or null | No | Where the page is. Required on a done publish, because it is what we print to the customer and what we then read back ourselves. It has to be an https address on the business's own website. |
| message | string or null | No | Anything the site wants to add, above all on a failure. It is stored with the rest of the body, word for word. |
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | PageReportRecorded | The report was recorded. state is what the page is now, so a client can stop asking about one that is done rather than infer it. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 413 | Error | The request body is over 16 KB, refused in front of the controller. |
| 422 | Error | Something you sent could not be used, and error.fields names it. Nothing is ever silently clamped: limit=5000 is a refusal rather than a page of fifty, because a client that asked for five thousand and was handed fifty without being told has written a loop that never terminates. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /businesses/{id}/partners
The partners list this site may show
Needs the partner_links ability. The whole of what goes on a partners page: the disclosure sentence to print above the list, the rel the owner chose for the links on their own site, the address of the record on picksyou.com, the page of their own site that shows it when one is registered, and one entry per live pair. enabled is false with an empty list when this business has no partners page, which is a true answer and not an error: print nothing. note is THIS business's own sentence about that partner and never the partner's about them. Nothing else about a partner is in it: no id, no plan, no count. Print what you last read and never fetch this on a visitor's page load; a public page must not wait on us. Never cached by anybody else: the body is two businesses' own words about each other.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | PartnersList | The list, possibly empty and possibly not enabled at all. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
POST /businesses/{id}/partners/page
Say which page of this site shows the list
Needs the partner_links ability. Register the permalink of the page showing the partners list, or send url null to clear it, which is what to send when the owner stops showing the list or deletes the page. url has to be in the body either way: clearing a registration is a real instruction and not an oversight, so a body that left the field out is a 422. The address has to be an https one on the business's own website and one this server is willing to fetch; anything else is a 422 that writes nothing at all. That address is the only fact about the site we hold, and it is what our own read-backs fetch: we look for an anchor to the partner's host and for the disclosure sentence, and that read is what writes the receipt. Registering a page changes what happens next and never what happened, so pairs already recorded against picksyou.com stay recorded there. A body over 16 KB is refused with body_too_large before the controller runs.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| id | path | Yes | The business, in either spelling: the integer id this API has always taken, or the opaque public_id every payload now also carries. The two can never be confused, because a public_id is fourteen characters and always starts with a letter. A segment that is neither never reaches a controller, and a business you may not read answers the same 404 as one that does not exist, whichever spelling was tried. |
What you send
| Field | Type | Required | What it means |
|---|---|---|---|
| url | string or null | Yes | The permalink of the page showing the list, over https and on the business's own website, or null to clear it. The field itself is required either way. |
| post_id | integer or null | No | The id of the post on your side, if your platform has one. |
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | PartnersPageRegistered | The address we now hold for this site, or nulls. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 413 | Error | The request body is over 16 KB, refused in front of the controller. |
| 422 | Error | Something you sent could not be used, and error.fields names it. Nothing is ever silently clamped: limit=5000 is a refusal rather than a page of fifty, because a client that asked for five thousand and was handed fifty without being told has written a loop that never terminates. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /partner/connections
Every shop this partner has, newest first
Cursor paginated on the id, because a partner walks this list while merchants are installing and uninstalling and an offset page two would skip or repeat rows as the table moves underneath it. next_before is null on the last page.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| status | query | No | Only shops in this state. |
| limit | query | No | Connections per page. |
| before | query | No | The id the previous page ended on. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | ConnectionPage | One page of connections, newest first. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 422 | Error | Something you sent could not be used, and error.fields names it. Nothing is ever silently clamped: limit=5000 is a refusal rather than a page of fifty, because a client that asked for five thousand and was handed fifty without being told has written a loop that never terminates. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
POST /partner/connections
Provision a shop
Idempotent on (partner, external_id), because that is what an app store sends: a platform replays its install webhook whenever it did not hear back, and a merchant who uninstalls on Friday and installs again on Monday sends the same identifier both times. A connection that is already here comes back with 200 and is not rewritten; an uninstalled one is switched back on with its whole history under it, without its old plan, because a reinstall is not a payment. 201 means this call wrote the row. The business fields are the free checker's own rules, so both doors feeding one table cannot disagree about what a business row may hold.
What goes in the address
What you send
| Field | Type | Required | What it means |
|---|---|---|---|
| external_id | string | Yes | Your own identifier for the merchant. The handle every later call uses. |
| name | string | Yes | The business name as the merchant writes it. |
| website | string or null | No | With or without a scheme; it is reduced to a bare host on our side, exactly as a typed one is. |
| city | string | Yes | |
| country | string | Yes | ISO 3166-1 alpha-2, and it must be a country the AI Overview vendor can search in: a country we cannot search is a country we cannot answer about. |
| language | string | Yes | The language the assistants are asked in. English only since 2026-09-05; any other value is refused with 422 and error.code validation_failed. |
| category | string | Yes | A pack slug, or other. Free text lines of business are checked in a general frame and say so through frame_uncertain. |
| category_label | string or null | No | Required when category is other. Letters, spaces and hyphens, up to 40. |
| meta | object or null | No | Anything you want to remember about this shop: a plan name on your side, a theme, an install source. At most 2 KB once encoded. Nothing personal: it survives an erasure. |
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | ConnectionWithBusiness | This shop was already connected. Nothing was written. |
| 201 | ConnectionWithBusiness | The connection was created by this call. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 409 | Error | business_owned: the business this shop dedupes onto already belongs to a picksyou.com account, and linking the two is a decision with a human in it rather than a guess inside a webhook. business_connected: another partner, or another shop of yours, already points at that business, and two live connections on one business would make "who is paying for this week" unanswerable. Neither writes anything. |
| 413 | Error | The request body is over 16 KB, refused in front of the controller. |
| 422 | Error | Something you sent could not be used, and error.fields names it. Nothing is ever silently clamped: limit=5000 is a refusal rather than a page of fifty, because a client that asked for five thousand and was handed fifty without being told has written a loop that never terminates. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
GET /partner/connections/{externalId}
One shop, by your own identifier for it
The handle in the URL is the partner's own identifier for the merchant, never our integer id, and the lookup is always scoped to the calling partner. Another partner's shop answers exactly as an identifier nobody has ever sent, and so does an erased one.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| externalId | path | Yes | Your own identifier for the merchant, the one you provisioned the shop with: a host or a slug, letters, digits, dot, dash and underscore, starting on a letter or a digit. It is stored and matched in LOWER CASE, so SHOP.MYSHOPIFY.COM and shop.myshopify.com are one shop on every database, and every answer prints the lower case form. THE SECOND FORM IS THE RETIRED ONE, redacted: followed by 64 hex characters, which is what an erased shop's identifier becomes and what the erasure endpoint answers with. Sending it back is how an erasure is idempotent, so the pattern admits both. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | ConnectionWithBusiness | The connection and the business behind it. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
DELETE /partner/connections/{externalId}
The app was removed
The connection is marked uninstalled, the plan goes back to free and the weekly machine is switched off tonight. THE BUSINESS STAYS, and so does every receipt under it: a merchant who uninstalls on Friday and installs again on Monday has to find their own history, which is what the platform's grace window is for. Uninstalling twice is uninstalling once, and the second call writes nothing, not even a second event.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| externalId | path | Yes | Your own identifier for the merchant, the one you provisioned the shop with: a host or a slug, letters, digits, dot, dash and underscore, starting on a letter or a digit. It is stored and matched in LOWER CASE, so SHOP.MYSHOPIFY.COM and shop.myshopify.com are one shop on every database, and every answer prints the lower case form. THE SECOND FORM IS THE RETIRED ONE, redacted: followed by 64 hex characters, which is what an erased shop's identifier becomes and what the erasure endpoint answers with. Sending it back is how an erasure is idempotent, so the pattern admits both. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | ConnectionOnly | The connection, uninstalled. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
PUT /partner/connections/{externalId}/plan
Put this shop on a plan, billed by the platform
The merchant pays the platform and the platform pays us, so no invoice of ours is issued and no payment of ours is recorded. The plan is written onto the connection and onto the business, and the weekly machine reads it exactly as it reads a plan somebody bought here. Send free to take a paid plan away. THIS DOOR IS SHUT BY DEFAULT and answers 503 plan_grants_paused until we open it, which is the opposite of every other switch in this product: it moves money on somebody else's word, so an undefined switch and an unreadable switch store are both a refusal. A shop that is not installed is 409 connection_not_active.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| externalId | path | Yes | Your own identifier for the merchant, the one you provisioned the shop with: a host or a slug, letters, digits, dot, dash and underscore, starting on a letter or a digit. It is stored and matched in LOWER CASE, so SHOP.MYSHOPIFY.COM and shop.myshopify.com are one shop on every database, and every answer prints the lower case form. THE SECOND FORM IS THE RETIRED ONE, redacted: followed by 64 hex characters, which is what an erased shop's identifier becomes and what the erasure endpoint answers with. Sending it back is how an erasure is idempotent, so the pattern admits both. |
What you send
| Field | Type | Required | What it means |
|---|---|---|---|
| plan | string | Yes | The family. free takes a paid plan away and stops the weekly machine tonight. |
| reference | string or null | No | Your own identifier for the charge, kept beside the grant. |
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | ConnectionWithBusiness | The connection and the business, both on the new plan. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 409 | Error | connection_not_active: that shop is uninstalled or erased, so its plan cannot be changed. |
| 413 | Error | The request body is over 16 KB, refused in front of the controller. |
| 422 | Error | Something you sent could not be used, and error.fields names it. Nothing is ever silently clamped: limit=5000 is a refusal rather than a page of fifty, because a client that asked for five thousand and was handed fifty without being told has written a loop that never terminates. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | api_paused (the whole API is off) or plan_grants_paused (this door is shut). Both carry retry_after and a Retry-After header. |
POST /partner/connections/{externalId}/redact
Erase this merchant
WHAT GOES: the name becomes a fixed placeholder, the aliases empty, the website, the Google profile link and the free text category label null, the dedupe key random so nothing can land on the row again, the plan free and the business inactive. The addresses left on a free check, the readings taken of the website, the visits, the crawler hits and the log upload records are deleted outright, and every appearance loses the alias text that matched it. The mails we sent about that shop and any internal alert naming it go with them; our own funnel keeps its counts and loses their details; our staff audit trail keeps its rows and the shop's name and domain inside them become "redacted". The public results page of every check on that shop stops opening. The shop's identifier becomes a hash of itself and this connection, so a shop that installed again after an earlier erasure can be erased again on its own row, and the meta is cleared. WHAT STAYS: the runs and the answers. They are our samples of what a public assistant said when asked a public question, the prompt names a city and a trade rather than the shop, and throwing them away would destroy the count of every other business in that city and week. Nothing kept points back at the merchant once the matched alias is gone. Erasing twice is erasing once, but only if you send the hashed identifier from your own list: the original identifier no longer names anything here, so replaying the platform's webhook with it is a 404, which is the honest answer and the point of the erasure.
What goes in the address
| Name | Where | Required | What it means |
|---|---|---|---|
| externalId | path | Yes | Your own identifier for the merchant, the one you provisioned the shop with: a host or a slug, letters, digits, dot, dash and underscore, starting on a letter or a digit. It is stored and matched in LOWER CASE, so SHOP.MYSHOPIFY.COM and shop.myshopify.com are one shop on every database, and every answer prints the lower case form. THE SECOND FORM IS THE RETIRED ONE, redacted: followed by 64 hex characters, which is what an erased shop's identifier becomes and what the erasure endpoint answers with. Sending it back is how an erasure is idempotent, so the pattern admits both. |
What you send
What comes back
| Answer | Shape | What it means |
|---|---|---|
| 200 | ConnectionOnly | The connection, erased. |
| 401 | Error | No live key on this request. A missing header, a scheme that is not Bearer, a key we have never seen, a revoked key, an expired key, a key on a closed account and a partner key whose partner is gone all answer this same body: telling them apart would tell somebody holding a stolen key whether it is still worth trying. Ask the person to paste the key again; do not retry the same one. |
| 403 | Error | The key is live and it may not do this: it does not carry the ability the endpoint asks for, or it is the wrong kind of key for the endpoint. Retrying will not help. A customer whose key cannot start checks has to mint a new one. |
| 404 | Error | There is nothing here for this key. A business that does not exist and a business this key may not see are the same answer, deliberately: a different answer for the two would let a stranger walk the integers and learn how many customers we have. |
| 405 | Error | That address does not answer this method. The router refuses this one before the route group runs, so the request id is minted by the error renderer instead of by the middleware; the header is there either way. |
| 429 | Error | Too many requests. retry_after is in the body and in the Retry-After header; wait that long, then try again. The cap that bites is per key. |
| 500 | Error | Something broke on our side. The body never carries a trace, a file path or an exception name, not even on a deployment left in debug mode. Quote the X-Request-Id when you write to us. |
| 503 | Error | The API is switched off right now, refused before anything is authenticated. retry_after is an hour. Back off and come back; do not treat it as a bad key and do not ask the person to re-enter anything. |
The shapes, field by field
Every object the answers above are built from. A dotted name is a field inside a field, and a name ending in two brackets is a field of each item of a list.
Error
THE ONE SHAPE EVERY REFUSAL TAKES. Write one branch for a failure and read error.code; error.message is English on every request and is for a person reading a log, not for a business owner reading a page.
| Field | Type | Always there | What it means |
|---|---|---|---|
| error | object | Yes | |
| error.code | string | Yes | The closed list. Adding one is a change to this file as well as to the code, and a test holds the two together. |
| error.message | string | Yes | One English sentence. Never a trace and never a file path. |
| error.fields | object | No | On validation_failed only: the field name that could not be used, and what was wrong with it. |
| error.retry_after | integer | No | On rate_limited, checks_limit_reached, api_paused and plan_grants_paused: seconds to wait. The same number is in the Retry-After header. |
| error.limit | integer | No | On checks_limit_reached only: how many new checks a day this key may start (per key for a customer key, per shop for a partner key). A client prints this number; it never parses the message for it. |
| error.paused | object | No | On business_paused only (402). This business has an open invoice, so its dashboard and every read about it are paused until the sum is paid. Nothing has been removed. The facts are the ones the panel's own paused page states, so a plugin can print a sentence its owner can act on: the sum, what it is made of, and where it is settled. |
| error.paused.amount | string | No | The open sum, as a decimal string. |
| error.paused.currency | string | No | |
| error.paused.vat | string | No | How much of the sum is VAT, as a decimal string. |
| error.paused.invoice_count | integer | No | |
| error.paused.invoices | array of object | No | Every open invoice, oldest first, by number, issue date and gross. They are what the sum adds up to. |
| error.paused.paused_since | string | No | The day the dashboard shut, or null. |
| error.paused.plan_ends_on | string | No | The day the plan ends if the sum is still open then, or null. |
| error.paused.billing_url | string | No | The page that settles it. |
Count
A COUNT AND EVERYTHING THAT MUST TRAVEL WITH IT. No number of appearances leaves this API on its own. hits out of n, the Wilson bounds as proportions, the half width of that interval in whole points, and the sentence we would print ourselves. Printing hits without n is a misuse of this API; docs/api/README.md has the worked example.
| Field | Type | Always there | What it means |
|---|---|---|---|
| hits | integer | Yes | How many answers named the business. |
| n | integer | Yes | How many answers counted. A vendor error and an empty AI Overview are in no n anywhere: they are not checks a business failed. |
| low | number | Yes | The lower Wilson bound, as a proportion between 0 and 1. |
| high | number | Yes | The upper Wilson bound, as a proportion between 0 and 1. |
| points | integer | Yes | Half the width of that interval, in whole points, which is the only form our own copy uses ("plus or minus 14 points"). |
| phrase | string | Yes | The sentence, in the reader's language, built by the same class every page and every mail of ours uses. An empty sample says "not checked yet" rather than a zero. |
Business
One business, the same object in every endpoint that names one. website is the businesses.domain column under the name a client would call it. The site key is not here: it belongs to /snippet.
| Field | Type | Always there | What it means |
|---|---|---|---|
| id | integer | Yes | |
| public_id | string | Yes | The opaque identifier this business is addressed by. It is beside id, never instead of it: id stays an integer and is not going anywhere. Either spelling may be sent in the {id} slot of any path, and a new integration should store this one, because it is what the owner's own pages carry. |
| name | string | Yes | |
| city | string | Yes | |
| country | string | Yes | ISO 3166-1 alpha-2, upper case. |
| language | string | Yes | ISO 639-1, lower case. |
| category | string | Yes | The pack slug this business is checked under, or other for a line of business we have no frame for. |
| category_label | string or null | Yes | The free text the owner typed, when category is other. |
| website | string or null | Yes | |
| plan | string or null | Yes | |
| plan_since | string or null | Yes | |
| created_at | string | Yes | |
| frame_uncertain | boolean | Yes | True when we could not place this line of business, so the questions are a general frame rather than a trade's own. A client printing blockers should say so, exactly as the receipt does. |
BusinessList
| Field | Type | Always there | What it means |
|---|---|---|---|
| businesses | array of Business | Yes | |
| businesses[].id | integer | Yes | |
| businesses[].public_id | string | Yes | The opaque identifier this business is addressed by. It is beside id, never instead of it: id stays an integer and is not going anywhere. Either spelling may be sent in the {id} slot of any path, and a new integration should store this one, because it is what the owner's own pages carry. |
| businesses[].name | string | Yes | |
| businesses[].city | string | Yes | |
| businesses[].country | string | Yes | ISO 3166-1 alpha-2, upper case. |
| businesses[].language | string | Yes | ISO 639-1, lower case. |
| businesses[].category | string | Yes | The pack slug this business is checked under, or other for a line of business we have no frame for. |
| businesses[].category_label | string or null | Yes | The free text the owner typed, when category is other. |
| businesses[].website | string or null | Yes | |
| businesses[].plan | string or null | Yes | |
| businesses[].plan_since | string or null | Yes | |
| businesses[].created_at | string | Yes | |
| businesses[].frame_uncertain | boolean | Yes | True when we could not place this line of business, so the questions are a general frame rather than a trade's own. A client printing blockers should say so, exactly as the receipt does. |
BusinessSummary
| Field | Type | Always there | What it means |
|---|---|---|---|
| business | object | Yes | One business, the same object in every endpoint that names one. website is the businesses.domain column under the name a client would call it. The site key is not here: it belongs to /snippet. |
| business.id | integer | Yes | |
| business.public_id | string | Yes | The opaque identifier this business is addressed by. It is beside id, never instead of it: id stays an integer and is not going anywhere. Either spelling may be sent in the {id} slot of any path, and a new integration should store this one, because it is what the owner's own pages carry. |
| business.name | string | Yes | |
| business.city | string | Yes | |
| business.country | string | Yes | ISO 3166-1 alpha-2, upper case. |
| business.language | string | Yes | ISO 639-1, lower case. |
| business.category | string | Yes | The pack slug this business is checked under, or other for a line of business we have no frame for. |
| business.category_label | string or null | Yes | The free text the owner typed, when category is other. |
| business.website | string or null | Yes | |
| business.plan | string or null | Yes | |
| business.plan_since | string or null | Yes | |
| business.created_at | string | Yes | |
| business.frame_uncertain | boolean | Yes | True when we could not place this line of business, so the questions are a general frame rather than a trade's own. A client printing blockers should say so, exactly as the receipt does. |
| summary | object | Yes | |
| summary.kind | string | Yes | weekly when a sampled week exists, free_check when the free check is all there is, none when there is nothing yet. |
| summary.headline | Headline or null | Yes | Null when kind is none. No sample means no information, never a zero. |
| summary.running | Running or null | Yes | Anything in flight for this business right now, of any kind. |
| summary.last_run_at | string or null | Yes | When this business was last checked, whoever asked for it. |
Summary
| Field | Type | Always there | What it means |
|---|---|---|---|
| kind | string | Yes | weekly when a sampled week exists, free_check when the free check is all there is, none when there is nothing yet. |
| headline | Headline or null | Yes | Null when kind is none. No sample means no information, never a zero. |
| running | Running or null | Yes | Anything in flight for this business right now, of any kind. |
| last_run_at | string or null | Yes | When this business was last checked, whoever asked for it. |
Headline
Running
| Field | Type | Always there | What it means |
|---|---|---|---|
| run_id | integer | Yes | |
| status | string | Yes | |
| expected_answers | integer | Yes | How many answers this run is planning to collect. |
| arrived | integer | Yes | How many have landed, errors and empties included. Draw a progress bar against expected_answers with this, never with n. |
WeekLedger
| Field | Type | Always there | What it means |
|---|---|---|---|
| weeks | array of Week | Yes | One row per week, oldest first, free checks included. |
| rolling | array of RollingWeek | Yes | The trailing four week window that ends at each sampled week. Free checks are never in here. |
Week
RollingWeek
SliceReport
One window, cut one way. dimension says which cut it is, window_weeks says how wide the window was asked to be (0 is everything we hold) and weeks lists the ISO weeks that were actually sampled inside it, oldest first. A week nobody sampled is not a column: a gap stays a gap, because a week with no check and a week with no naming are different facts. It counts the same runs the headline counts, which is the weekly check and the baseline check the first payment bought, and nothing else: a free check somebody ran on the public checker is a different size of check and is in no figure here.
| Field | Type | Always there | What it means |
|---|---|---|---|
| dimension | string | Yes | |
| window_weeks | integer | Yes | The window asked for, in weeks. 0 is every week we hold. |
| weeks | array of string | Yes | The sampled ISO weeks this cut is drawn over, oldest first. |
| slices | array of Slice | Yes | One row per assistant, in the roster order of the newest sampled run, or one row per question, fewest namings first. Neither order is a claim about anything, and there is no key here that says it is. |
| slices[].key | string | Yes | The assistant's own name in our data, or the identifier of the question. Stable, and what a client joins two calls on. |
| slices[].label | string | Yes | The assistant as a reader knows it, or the question word for word as we asked it that week. A template edited later does not rewrite what a past week was asked. |
| slices[].count | integer | Yes | How many answers in this row named the business. |
| slices[].n | integer | Yes | How many answers this row counted. Weeks the assistant or the question was not asked in are not in it, and neither is a vendor error. |
| slices[].low | integer | Yes | The lower end of the usual range, as a whole count out of the same n. Our own sentence for a row reads "named in 27 of 90, usually between 19 and 36 of 90", and these are those two numbers. |
| slices[].high | integer | Yes | The upper end of the usual range, as a whole count out of n. |
| slices[].grade | string | Yes | How much weight this row will bear, read off the width of its range and the size of its n. too_thin is every n under ten and every empty sample. A row is printed with its sentence or it is not printed. |
| slices[].sentence | string | Yes | The sentence we would print ourselves, in the reader's language. About our sample, never about the business: there is no verdict here and no adjective about anybody. |
| slices[].cells | array of SliceCell | Yes | One per week in the report's weeks list, in the same order. |
| slices[].cells[].week | string | Yes | The ISO week, as 2026-W36. |
| slices[].cells[].asked | boolean | Yes | |
| slices[].cells[].count | integer | Yes | |
| slices[].cells[].n | integer | Yes |
Slice
One assistant, or one question, over the whole window. count out of n, the usual range as whole counts of that same n, and how much weight the row will bear. The range is ONE range over the window's totals, never one per week: a single week of a single assistant is nine or fifteen answers, and six ranges drawn across a row would each say almost nothing and together look like a trend.
| Field | Type | Always there | What it means |
|---|---|---|---|
| key | string | Yes | The assistant's own name in our data, or the identifier of the question. Stable, and what a client joins two calls on. |
| label | string | Yes | The assistant as a reader knows it, or the question word for word as we asked it that week. A template edited later does not rewrite what a past week was asked. |
| count | integer | Yes | How many answers in this row named the business. |
| n | integer | Yes | How many answers this row counted. Weeks the assistant or the question was not asked in are not in it, and neither is a vendor error. |
| low | integer | Yes | The lower end of the usual range, as a whole count out of the same n. Our own sentence for a row reads "named in 27 of 90, usually between 19 and 36 of 90", and these are those two numbers. |
| high | integer | Yes | The upper end of the usual range, as a whole count out of n. |
| grade | string | Yes | How much weight this row will bear, read off the width of its range and the size of its n. too_thin is every n under ten and every empty sample. A row is printed with its sentence or it is not printed. |
| sentence | string | Yes | The sentence we would print ourselves, in the reader's language. About our sample, never about the business: there is no verdict here and no adjective about anybody. |
| cells | array of SliceCell | Yes | One per week in the report's weeks list, in the same order. |
| cells[].week | string | Yes | The ISO week, as 2026-W36. |
| cells[].asked | boolean | Yes | |
| cells[].count | integer | Yes | |
| cells[].n | integer | Yes |
SliceCell
One week of one row. asked false means this assistant was not on that week's roster, or this question was not in that week's pack: the counts are zero and the week is in no total, because nobody asked. There is no range on a cell and no difference between two cells.
| Field | Type | Always there | What it means |
|---|---|---|---|
| week | string | Yes | The ISO week, as 2026-W36. |
| asked | boolean | Yes | |
| count | integer | Yes | |
| n | integer | Yes |
Run
RunPage
| Field | Type | Always there | What it means |
|---|---|---|---|
| runs | array of Run | Yes | |
| next_before | integer or null | Yes | Pass this back as ?before= for the next page. Null on the last one. |
RunDetail
| Field | Type | Always there | What it means |
|---|---|---|---|
| run | Count or object | Yes | |
| questions | array of Question | Yes | The questions this run asked, word for word as it asked them. |
| questions[].question_position | integer | Yes | WHICH QUESTION OF THE PACK THIS IS, counting from one. It is the number of a question and nothing to do with a place in any list; the answer cards carry the same key, so the two lists join on it. |
| questions[].text | string | Yes | |
| answers | array of AnswerCard | Yes | |
| answers[].question_position | integer | Yes | |
| answers[].question | string | Yes | The question as it was asked, read off the answer rather than the pack. |
| answers[].assistant | object | Yes | |
| answers[].assistant.id | string | Yes | |
| answers[].assistant.label | string | Yes | What to print, in the language that was asked for. |
| answers[].repeat | integer | Yes | Which repeat of that question this answer is, counting from zero. |
| answers[].status | string | Yes | ok is an answer with words in it. empty is an answer that arrived with nothing (an AI Overview that did not render). error is a vendor that failed. |
| answers[].verdict | string | Yes | picked is a confirmed naming. possible is a match still waiting for a person, and it counts for nothing: it is in n and not in hits. not_picked is an answer that named somebody else. empty and error are in no n at all. |
| answers[].text | string | Yes | The assistant's own words, plain, as they were stored. |
| answers[].excerpt | string | Yes | The first 320 characters of the same text, cut on a word. |
| answers[].citations | array of string | Yes | The URLs the assistant cited, as it sent them. |
| answers[].answered_at | string or null | Yes |
Question
| Field | Type | Always there | What it means |
|---|---|---|---|
| question_position | integer | Yes | WHICH QUESTION OF THE PACK THIS IS, counting from one. It is the number of a question and nothing to do with a place in any list; the answer cards carry the same key, so the two lists join on it. |
| text | string | Yes |
AnswerPage
| Field | Type | Always there | What it means |
|---|---|---|---|
| run_id | integer or null | Yes | Null when this business has nothing to show yet. |
| answers | array of AnswerCard | Yes | |
| answers[].question_position | integer | Yes | |
| answers[].question | string | Yes | The question as it was asked, read off the answer rather than the pack. |
| answers[].assistant | object | Yes | |
| answers[].assistant.id | string | Yes | |
| answers[].assistant.label | string | Yes | What to print, in the language that was asked for. |
| answers[].repeat | integer | Yes | Which repeat of that question this answer is, counting from zero. |
| answers[].status | string | Yes | ok is an answer with words in it. empty is an answer that arrived with nothing (an AI Overview that did not render). error is a vendor that failed. |
| answers[].verdict | string | Yes | picked is a confirmed naming. possible is a match still waiting for a person, and it counts for nothing: it is in n and not in hits. not_picked is an answer that named somebody else. empty and error are in no n at all. |
| answers[].text | string | Yes | The assistant's own words, plain, as they were stored. |
| answers[].excerpt | string | Yes | The first 320 characters of the same text, cut on a word. |
| answers[].citations | array of string | Yes | The URLs the assistant cited, as it sent them. |
| answers[].answered_at | string or null | Yes |
AnswerCard
One answer and the verdict the panel gave it. No markup of ours is in here: the panel's own card carries a highlighted excerpt, and a client writing our markup into somebody's theme is not something we will ask for. The vendor's stored body never leaves the building either.
| Field | Type | Always there | What it means |
|---|---|---|---|
| question_position | integer | Yes | |
| question | string | Yes | The question as it was asked, read off the answer rather than the pack. |
| assistant | object | Yes | |
| assistant.id | string | Yes | |
| assistant.label | string | Yes | What to print, in the language that was asked for. |
| repeat | integer | Yes | Which repeat of that question this answer is, counting from zero. |
| status | string | Yes | ok is an answer with words in it. empty is an answer that arrived with nothing (an AI Overview that did not render). error is a vendor that failed. |
| verdict | string | Yes | picked is a confirmed naming. possible is a match still waiting for a person, and it counts for nothing: it is in n and not in hits. not_picked is an answer that named somebody else. empty and error are in no n at all. |
| text | string | Yes | The assistant's own words, plain, as they were stored. |
| excerpt | string | Yes | The first 320 characters of the same text, cut on a word. |
| citations | array of string | Yes | The URLs the assistant cited, as it sent them. |
| answered_at | string or null | Yes |
Assistant
| Field | Type | Always there | What it means |
|---|---|---|---|
| id | string | Yes | |
| label | string | Yes | What to print, in the language that was asked for. |
BlockerReport
| Field | Type | Always there | What it means |
|---|---|---|---|
| run_id | integer or null | Yes | |
| checked_at | string or null | Yes | When this reading was taken. Null when the checks have not run: that is not a clean bill of health and must not be drawn as one. |
| blockers | array of Blocker | Yes | The tasks, in the order an owner should read them. |
| blockers[].kind | string | Yes | |
| blockers[].status | string | Yes | |
| blockers[].tag | string | Yes | Where the reading came from: measured is our own crawl of the site, vendor is a third party lookup, research is what the published research says about this check. |
| blockers[].title | string | Yes | |
| blockers[].body | string | Yes | Three sentences joined into one paragraph: what we found, why it matters, and what to do about it. Each of them ends in its own full stop, so a client can print the whole field as a paragraph. A client that printed only the title would be printing a scolding with no way out of it. |
| blockers[].state | string | Yes | not_started is a failing check. in_progress is a warning. fixed is a check that was failing or warning at the FIRST reading we ever took of this business and is fine now, which is the owner's own work since we started watching. |
| rest | array of Reading | Yes | Everything else we looked at and found nothing wrong with. |
| rest[].kind | string | Yes | |
| rest[].status | string | Yes | |
| rest[].tag | string | Yes | Where the reading came from: measured is our own crawl of the site, vendor is a third party lookup, research is what the published research says about this check. |
| rest[].title | string | Yes | |
| rest[].body | string | Yes | Three sentences joined into one paragraph: what we found, why it matters, and what to do about it. Each of them ends in its own full stop, so a client can print the whole field as a paragraph. A client that printed only the title would be printing a scolding with no way out of it. |
Reading
| Field | Type | Always there | What it means |
|---|---|---|---|
| kind | string | Yes | |
| status | string | Yes | |
| tag | string | Yes | Where the reading came from: measured is our own crawl of the site, vendor is a third party lookup, research is what the published research says about this check. |
| title | string | Yes | |
| body | string | Yes | Three sentences joined into one paragraph: what we found, why it matters, and what to do about it. Each of them ends in its own full stop, so a client can print the whole field as a paragraph. A client that printed only the title would be printing a scolding with no way out of it. |
Blocker
A reading with a state on it. It is written out in full rather than as an allOf over Reading, and the reason is worth knowing: a JSON Schema allOf branch is applied to the WHOLE instance, so Reading's additionalProperties false refused the state key and every strict validator (ajv, openapi-core, Spectral) rejected every blockers answer this API gives.
| Field | Type | Always there | What it means |
|---|---|---|---|
| kind | string | Yes | |
| status | string | Yes | |
| tag | string | Yes | Where the reading came from: measured is our own crawl of the site, vendor is a third party lookup, research is what the published research says about this check. |
| title | string | Yes | |
| body | string | Yes | Three sentences joined into one paragraph: what we found, why it matters, and what to do about it. Each of them ends in its own full stop, so a client can print the whole field as a paragraph. A client that printed only the title would be printing a scolding with no way out of it. |
| state | string | Yes | not_started is a failing check. in_progress is a warning. fixed is a check that was failing or warning at the FIRST reading we ever took of this business and is fine now, which is the owner's own work since we started watching. |
NamedLedger
| Field | Type | Always there | What it means |
|---|---|---|---|
| week | string or null | Yes | The ISO week these counts are for, as 2026-W36. |
| n | integer | Yes | How many answers this week's counts came out of. |
| compare_n | integer | Yes | How many answers the four weeks ago column came out of. |
| rows | array of NamedRow | Yes | |
| rows[].name | string | Yes | |
| rows[].you | boolean | Yes | True on the row that is this business. |
| rows[].this_week | integer | Yes | |
| rows[].four_weeks | integer | Yes | |
| rows[].change | integer or null | Yes | this_week minus four_weeks, or null when there is nothing to compare. |
NamedRow
A name and how many answers named it. There is no ordinal key in this object and there never will be: an assistant holds no league table for anybody to be third of, so we do not publish a place in one.
| Field | Type | Always there | What it means |
|---|---|---|---|
| name | string | Yes | |
| you | boolean | Yes | True on the row that is this business. |
| this_week | integer | Yes | |
| four_weeks | integer | Yes | |
| change | integer or null | Yes | this_week minus four_weeks, or null when there is nothing to compare. |
ReportList
| Field | Type | Always there | What it means |
|---|---|---|---|
| reports | array of ClosedMonth | Yes | The months that have closed, newest first. A month still running is not here at all. |
| reports[].period | string | Yes | The month, as four digits, a hyphen and two. It is the id of this report. |
| reports[].hits | integer or null | Yes | How many of the month's answers named the business. |
| reports[].n | integer or null | Yes | How many answers that count came out of. |
| reports[].low | integer or null | Yes | The low end of the usual range, as a whole count of the same n, not a proportion and not a share of a hundred. Null with high on a month of fewer than two sampled weeks: a band drawn around a single reading is not something we publish. |
| reports[].high | integer or null | Yes | The high end of the same range, in the same whole counts. |
| reports[].sampled_weeks | integer | Yes | How many Mondays of the month were actually sampled. |
| reports[].closed_at | string or null | Yes | When the figures were frozen. |
| reports[].amended_at | string or null | Yes | When the month was corrected, if it ever was. An amended report keeps its first figures and prints both, so a client holding the original can see that something moved and when. |
ClosedMonth
One month, measured once on the day it closed and never counted again. hits and n are null together on a month nobody sampled, because no sample is not a measurement of nothing.
| Field | Type | Always there | What it means |
|---|---|---|---|
| period | string | Yes | The month, as four digits, a hyphen and two. It is the id of this report. |
| hits | integer or null | Yes | How many of the month's answers named the business. |
| n | integer or null | Yes | How many answers that count came out of. |
| low | integer or null | Yes | The low end of the usual range, as a whole count of the same n, not a proportion and not a share of a hundred. Null with high on a month of fewer than two sampled weeks: a band drawn around a single reading is not something we publish. |
| high | integer or null | Yes | The high end of the same range, in the same whole counts. |
| sampled_weeks | integer | Yes | How many Mondays of the month were actually sampled. |
| closed_at | string or null | Yes | When the figures were frozen. |
| amended_at | string or null | Yes | When the month was corrected, if it ever was. An amended report keeps its first figures and prints both, so a client holding the original can see that something moved and when. |
MonthReport
A closed month, whole, exactly as it was frozen. THIS OBJECT IS NOT CLOSED, and that is deliberate: the document belongs to the month report rather than to this contract, and a schema that refused a field the document grew would make a client's validator reject a customer's own September. The keys below are the ones every report carries and they will not be removed; read them by name and ignore anything else you meet. The nested objects are the sections the paper prints, and each one is documented in docs/api/README.md rather than pinned key by key here. TWO KEYS DEPEND ON THE KEY ASKING: pages and pages_published are present only for a key carrying the pages ability, and are absent rather than empty for every other key and for every partner key, since an empty list would read as a claim that no page was published.
| Field | Type | Always there | What it means |
|---|---|---|---|
| period | string | Yes | The month, as four digits, a hyphen and two. |
| period_start | string | No | |
| period_end | string | No | |
| business | object | No | The name, town, country, trade and plan as they were that month. |
| hits | integer or null | Yes | How many of the month's answers named the business. |
| n | integer or null | Yes | How many answers that count came out of. |
| low | integer or null | Yes | The low end of the usual range, as a whole count of the same n. |
| high | integer or null | Yes | The high end of the same range, in the same whole counts. |
| sampled_weeks | integer | Yes | |
| mondays | array of string | No | Every Monday of the month, sampled or not. |
| weeks | array of object | Yes | One entry a sampled Monday, with its own count, its n, its cut by assistant and the clock the run ran between. |
| engines | object | No | The assistants of that month with their totals, in the enum's order. |
| questions | integer | No | How many questions the month actually asked, counted rather than assumed. |
| assistants | integer | No | |
| repeats | integer | No | How many times each question went to each assistant. |
| answers_a_week | integer or null | No | |
| excerpts | object | No | Two answers quoted word for word, the first sampled Monday and the last. The excerpt carries one mark tag around the matched name, because it is the document's own excerpt; strip it if your surface does not want it. |
| blockers | object | No | What was in the way that month, and what stopped being. |
| visits | object | No | People who reached a page carrying the snippet, by assistant, and what each assistant's crawler fetched. |
| named | object or null | No | The businesses named beside this one. Null on a plan that does not carry the four week list. |
| previous | object or null | No | The month before, and whether the two usual ranges overlap. Two ranges that overlap are the usual swing; two that do not are the one case where we say a month was different. Never a cause. |
| reviewed | integer | No | How many matches a person confirmed that month. |
| held | integer | No | How many matches are still waiting for a person. They are in n and in no count. |
| closed_at | string or null | Yes | When the figures were frozen. |
Receipts
| Field | Type | Always there | What it means |
|---|---|---|---|
| days | integer | Yes | The window, in days. Thirty. |
| installed | boolean | Yes | Whether anything has ever arrived. A snippet on a page nobody arrives at from an assistant sends nothing, by design, so this is the honest thing we can say about it. |
| since | string or null | Yes | The first visit we ever recorded for this business. |
| visits | object | Yes | |
| visits.total | integer | Yes | |
| visits.by_assistant | array of AssistantVisits | Yes | One row per assistant that was actually seen. No rows of zeroes. |
| visits.by_assistant[].assistant | string | Yes | |
| visits.by_assistant[].label | string | Yes | |
| visits.by_assistant[].count | integer | Yes | |
| visits.by_day | array of DayVisits | Yes | Thirty days, oldest first, every one of them printed. |
| visits.by_day[].date | string | Yes | |
| visits.by_day[].count | integer | Yes |
Visits
| Field | Type | Always there | What it means |
|---|---|---|---|
| total | integer | Yes | |
| by_assistant | array of AssistantVisits | Yes | One row per assistant that was actually seen. No rows of zeroes. |
| by_assistant[].assistant | string | Yes | |
| by_assistant[].label | string | Yes | |
| by_assistant[].count | integer | Yes | |
| by_day | array of DayVisits | Yes | Thirty days, oldest first, every one of them printed. |
| by_day[].date | string | Yes | |
| by_day[].count | integer | Yes |
AssistantVisits
| Field | Type | Always there | What it means |
|---|---|---|---|
| assistant | string | Yes | |
| label | string | Yes | |
| count | integer | Yes |
DayVisits
| Field | Type | Always there | What it means |
|---|---|---|---|
| date | string | Yes | |
| count | integer | Yes |
Snippet
| Field | Type | Always there | What it means |
|---|---|---|---|
| site_key | string | Yes | The name this business's receipts are filed under. It sits in a script tag on the owner's own pages, so it is not a secret, but it is behind the same key check as everything else here. |
| script_url | string | Yes | The script tag's src. Built from this deployment's own routes. |
| beacon_url | string | Yes | Where the script posts an arrival. |
| enabled | boolean | Yes | Whether we are recording visits at all right now. False means a client should say counting is paused rather than show an owner a snippet that is quietly doing nothing. |
| instruction | string | Yes | One sentence to print beside the switch, in the owner's language. |
PagesQueue
| Field | Type | Always there | What it means |
|---|---|---|---|
| publish | array of PageToPublish | Yes | Pages approved and not yet reported as up. Empty is the ordinary answer. |
| publish[].page | string | Yes | The page's own id, which is what a report names. |
| publish[].business | integer | Yes | The business this page belongs to, so a client can refuse an item that is not the one it asked about. |
| publish[].approval | string | Yes | The approvals row that authorised it. It is on the customer's own ledger, and everything we later write about this page hangs off it. |
| publish[].title | string | Yes | The title as the customer approved it, or edited it. |
| publish[].slug | string | Yes | A suggestion for the address. WordPress makes it unique on its own side, and nothing here depends on the one you end up with. |
| publish[].html | string | Yes | The text of the approvals row itself: h2, h3, p, ul, li and links to the business's own website, with the disclosure line as the last paragraph. Put it through your platform's own sanitiser anyway. |
| publish[].approved_at | string or null | Yes | When the customer approved it. |
| publish[].post_type_hint | string | Yes | What we would choose if the site had no preference. The site owner's own setting wins; this is a word, not an instruction. |
| unpublish | array of PageToUnpublish | Yes | Pages whose owner pressed Undo and which this site has not reported taking down yet. |
| unpublish[].page | string | Yes | The page's own id, which is what a report names. |
| unpublish[].business | integer | Yes | The business this page belongs to. |
| unpublish[].approval | string | Yes | The approvals row of the UNDO, not of the publication. What the site says about taking a page down belongs under the click that asked for it. |
| unpublish[].wp_post_id | integer or null | Yes | The post id you reported when you put it up, handed back so you do not have to keep a second list. Null when we never got one. |
PageToPublish
| Field | Type | Always there | What it means |
|---|---|---|---|
| page | string | Yes | The page's own id, which is what a report names. |
| business | integer | Yes | The business this page belongs to, so a client can refuse an item that is not the one it asked about. |
| approval | string | Yes | The approvals row that authorised it. It is on the customer's own ledger, and everything we later write about this page hangs off it. |
| title | string | Yes | The title as the customer approved it, or edited it. |
| slug | string | Yes | A suggestion for the address. WordPress makes it unique on its own side, and nothing here depends on the one you end up with. |
| html | string | Yes | The text of the approvals row itself: h2, h3, p, ul, li and links to the business's own website, with the disclosure line as the last paragraph. Put it through your platform's own sanitiser anyway. |
| approved_at | string or null | Yes | When the customer approved it. |
| post_type_hint | string | Yes | What we would choose if the site had no preference. The site owner's own setting wins; this is a word, not an instruction. |
PageToUnpublish
| Field | Type | Always there | What it means |
|---|---|---|---|
| page | string | Yes | The page's own id, which is what a report names. |
| business | integer | Yes | The business this page belongs to. |
| approval | string | Yes | The approvals row of the UNDO, not of the publication. What the site says about taking a page down belongs under the click that asked for it. |
| wp_post_id | integer or null | Yes | The post id you reported when you put it up, handed back so you do not have to keep a second list. Null when we never got one. |
PageReport
| Field | Type | Always there | What it means |
|---|---|---|---|
| action | string | Yes | Which of the two lists this item came off. |
| outcome | string | Yes | done means the site did it. failed means it could not, nothing moves, and the item stays on the queue for the next sync. |
| post_id | integer or null | No | The id of the post you made, or the one you updated. |
| url | string or null | No | Where the page is. Required on a done publish, because it is what we print to the customer and what we then read back ourselves. It has to be an https address on the business's own website. |
| message | string or null | No | Anything the site wants to add, above all on a failure. It is stored with the rest of the body, word for word. |
PageReportRecorded
| Field | Type | Always there | What it means |
|---|---|---|---|
| page | string | Yes | The page the report was filed against. |
| action | string | Yes | |
| outcome | string | Yes | |
| state | string | Yes | What the page is now. approved is waiting for this site, published is up, unpublished is down again. A report that arrived twice answers the same word as the first one. |
PartnersList
| Field | Type | Always there | What it means |
|---|---|---|---|
| enabled | boolean | Yes | Whether this business has a partners page at all. False with an empty list is the ordinary answer for one that never opted in, and it is what to print nothing on. |
| rel | string | Yes | What the owner chose for the links on their own site. followed means no rel attribute at all on the anchor; nofollow means exactly rel="nofollow". Links on picksyou.com's own record are always nofollow, whatever this says. |
| record_url | string | Yes | The record of this list on picksyou.com, which exists whether or not the site shows anything. |
| site_page_url | string or null | Yes | The page of the business's own site that shows the list, as that site last told us, or null when none is registered. |
| disclosure | string | Yes | One sentence that says what the page is. Print it above the list, on every render, unchanged. A page without it is not a partners page, and our read-back looks for it. |
| method_url | string | Yes | The one link of ours the page carries, explaining how partner links work. Give it rel="nofollow". |
| partners | array of PartnerEntry | Yes | One entry per live pair, in the order the pairs were made. |
| partners[].name | string | Yes | The partner's name, as the link's text. |
| partners[].url | string | Yes | The partner's own website, as the link's address. |
| partners[].trade | string | Yes | The partner's trade, in the same word every question of ours uses. |
| partners[].place | string | Yes | The partner's town, or the word online. |
| partners[].note | string | Yes | THIS business's own sentence about that partner, never the partner's about them. It is their own words and it is what makes the entry a recommendation rather than a list of links. Print it as text; it carries no markup and none is allowed in it. |
| partners[].since | string | Yes | The month the pair was made, as 2026-09. |
PartnerEntry
| Field | Type | Always there | What it means |
|---|---|---|---|
| name | string | Yes | The partner's name, as the link's text. |
| url | string | Yes | The partner's own website, as the link's address. |
| trade | string | Yes | The partner's trade, in the same word every question of ours uses. |
| place | string | Yes | The partner's town, or the word online. |
| note | string | Yes | THIS business's own sentence about that partner, never the partner's about them. It is their own words and it is what makes the entry a recommendation rather than a list of links. Print it as text; it carries no markup and none is allowed in it. |
| since | string | Yes | The month the pair was made, as 2026-09. |
PartnersPageReport
| Field | Type | Always there | What it means |
|---|---|---|---|
| url | string or null | Yes | The permalink of the page showing the list, over https and on the business's own website, or null to clear it. The field itself is required either way. |
| post_id | integer or null | No | The id of the post on your side, if your platform has one. |
PartnersPageRegistered
| Field | Type | Always there | What it means |
|---|---|---|---|
| site_page_url | string or null | Yes | The address we now hold, or null after a clearing. |
| post_id | integer or null | Yes | The post id we now hold, or null. |
| registered_at | string or null | Yes | When it was registered, or null after a clearing. |
CheckAccepted
| Field | Type | Always there | What it means |
|---|---|---|---|
| outcome | string | Yes | started is a new run. retried is a failed run pushed back onto the queue, keeping the answers it already had. reused is a fresh run that already existed, and nothing was created or spent. queued is a run dated for tomorrow because today's budget is committed. |
| run | object | Yes | |
| run.id | integer | Yes | |
| run.public_id | string | Yes | The opaque identifier of the run that was started, beside id and never instead of it. |
| run.kind | string | Yes | The same word the read endpoints use: a free check is free_check, a paid week is weekly, the first paid week is baseline. |
| run.status | string | Yes | |
| run.scheduled_for | string or null | Yes | The day the run belongs to. Tomorrow, on the queued outcome. |
| run.expected_answers | integer or null | Yes | Null until the fan out is planned, seconds later. Null rather than zero, because a zero reads to a progress bar as a run with nothing in it. |
| run.poll_url | string | Yes | The run endpoint to ask again in a few seconds. It spells the two ids as integers, which every version of this API answers. |
| run.receipt_url | string or null | Yes | The public results page for this run, which a client may put in front of a business owner as a link. |
CheckRun
| Field | Type | Always there | What it means |
|---|---|---|---|
| id | integer | Yes | |
| public_id | string | Yes | The opaque identifier of the run that was started, beside id and never instead of it. |
| kind | string | Yes | The same word the read endpoints use: a free check is free_check, a paid week is weekly, the first paid week is baseline. |
| status | string | Yes | |
| scheduled_for | string or null | Yes | The day the run belongs to. Tomorrow, on the queued outcome. |
| expected_answers | integer or null | Yes | Null until the fan out is planned, seconds later. Null rather than zero, because a zero reads to a progress bar as a run with nothing in it. |
| poll_url | string | Yes | The run endpoint to ask again in a few seconds. It spells the two ids as integers, which every version of this API answers. |
| receipt_url | string or null | Yes | The public results page for this run, which a client may put in front of a business owner as a link. |
ConnectionRequest
| Field | Type | Always there | What it means |
|---|---|---|---|
| external_id | string | Yes | Your own identifier for the merchant. The handle every later call uses. |
| name | string | Yes | The business name as the merchant writes it. |
| website | string or null | No | With or without a scheme; it is reduced to a bare host on our side, exactly as a typed one is. |
| city | string | Yes | |
| country | string | Yes | ISO 3166-1 alpha-2, and it must be a country the AI Overview vendor can search in: a country we cannot search is a country we cannot answer about. |
| language | string | Yes | The language the assistants are asked in. English only since 2026-09-05; any other value is refused with 422 and error.code validation_failed. |
| category | string | Yes | A pack slug, or other. Free text lines of business are checked in a general frame and say so through frame_uncertain. |
| category_label | string or null | No | Required when category is other. Letters, spaces and hyphens, up to 40. |
| meta | object or null | No | Anything you want to remember about this shop: a plan name on your side, a theme, an install source. At most 2 KB once encoded. Nothing personal: it survives an erasure. |
PlanRequest
| Field | Type | Always there | What it means |
|---|---|---|---|
| plan | string | Yes | The family. free takes a paid plan away and stops the weekly machine tonight. |
| reference | string or null | No | Your own identifier for the charge, kept beside the grant. |
Connection
| Field | Type | Always there | What it means |
|---|---|---|---|
| id | integer | Yes | |
| external_id | string | Yes | Your identifier for the merchant, or, after an erasure, a hash of it prefixed with "redacted:". |
| status | string | Yes | |
| plan | ConnectionPlan or null | Yes | Null when the shop is on free, rather than an object full of nulls: "this shop is not paying" is one fact and reads as one. |
| installed_at | string or null | Yes | |
| uninstalled_at | string or null | Yes | |
| redacted_at | string or null | Yes | |
| business_id | integer | Yes | |
| business_public_id | string | Yes | The opaque identifier of the same business, beside business_id and never instead of it. Either spelling may be sent in the {id} slot of a business path. |
ConnectionPlan
| Field | Type | Always there | What it means |
|---|---|---|---|
| family | string or null | Yes | |
| code | string | Yes | The family's active price code, which is what the weekly machine reads, whether the money came through us or through a platform. |
| reference | string or null | Yes | |
| granted_at | string or null | Yes |
ConnectionPage
| Field | Type | Always there | What it means |
|---|---|---|---|
| connections | array of Connection | Yes | |
| connections[].id | integer | Yes | |
| connections[].external_id | string | Yes | Your identifier for the merchant, or, after an erasure, a hash of it prefixed with "redacted:". |
| connections[].status | string | Yes | |
| connections[].plan | ConnectionPlan or null | Yes | Null when the shop is on free, rather than an object full of nulls: "this shop is not paying" is one fact and reads as one. |
| connections[].installed_at | string or null | Yes | |
| connections[].uninstalled_at | string or null | Yes | |
| connections[].redacted_at | string or null | Yes | |
| connections[].business_id | integer | Yes | |
| connections[].business_public_id | string | Yes | The opaque identifier of the same business, beside business_id and never instead of it. Either spelling may be sent in the {id} slot of a business path. |
| next_before | integer or null | Yes |
ConnectionWithBusiness
| Field | Type | Always there | What it means |
|---|---|---|---|
| connection | object | Yes | |
| connection.id | integer | Yes | |
| connection.external_id | string | Yes | Your identifier for the merchant, or, after an erasure, a hash of it prefixed with "redacted:". |
| connection.status | string | Yes | |
| connection.plan | ConnectionPlan or null | Yes | Null when the shop is on free, rather than an object full of nulls: "this shop is not paying" is one fact and reads as one. |
| connection.installed_at | string or null | Yes | |
| connection.uninstalled_at | string or null | Yes | |
| connection.redacted_at | string or null | Yes | |
| connection.business_id | integer | Yes | |
| connection.business_public_id | string | Yes | The opaque identifier of the same business, beside business_id and never instead of it. Either spelling may be sent in the {id} slot of a business path. |
| business | object | Yes | One business, the same object in every endpoint that names one. website is the businesses.domain column under the name a client would call it. The site key is not here: it belongs to /snippet. |
| business.id | integer | Yes | |
| business.public_id | string | Yes | The opaque identifier this business is addressed by. It is beside id, never instead of it: id stays an integer and is not going anywhere. Either spelling may be sent in the {id} slot of any path, and a new integration should store this one, because it is what the owner's own pages carry. |
| business.name | string | Yes | |
| business.city | string | Yes | |
| business.country | string | Yes | ISO 3166-1 alpha-2, upper case. |
| business.language | string | Yes | ISO 639-1, lower case. |
| business.category | string | Yes | The pack slug this business is checked under, or other for a line of business we have no frame for. |
| business.category_label | string or null | Yes | The free text the owner typed, when category is other. |
| business.website | string or null | Yes | |
| business.plan | string or null | Yes | |
| business.plan_since | string or null | Yes | |
| business.created_at | string | Yes | |
| business.frame_uncertain | boolean | Yes | True when we could not place this line of business, so the questions are a general frame rather than a trade's own. A client printing blockers should say so, exactly as the receipt does. |
ConnectionOnly
| Field | Type | Always there | What it means |
|---|---|---|---|
| connection | object | Yes | |
| connection.id | integer | Yes | |
| connection.external_id | string | Yes | Your identifier for the merchant, or, after an erasure, a hash of it prefixed with "redacted:". |
| connection.status | string | Yes | |
| connection.plan | ConnectionPlan or null | Yes | Null when the shop is on free, rather than an object full of nulls: "this shop is not paying" is one fact and reads as one. |
| connection.installed_at | string or null | Yes | |
| connection.uninstalled_at | string or null | Yes | |
| connection.redacted_at | string or null | Yes | |
| connection.business_id | integer | Yes | |
| connection.business_public_id | string | Yes | The opaque identifier of the same business, beside business_id and never instead of it. Either spelling may be sent in the {id} slot of a business path. |
Me
A customer key answers one shape and a partner key the other.
MeCustomer
| Field | Type | Always there | What it means |
|---|---|---|---|
| kind | string | Yes | |
| key | object | Yes | The key as its holder sees it. Never the token and never its hash. |
| key.name | string | Yes | What the person who minted it called it. |
| key.prefix | string | Yes | The first twelve characters, which is enough to tell two keys apart. |
| key.abilities | array of string | Yes | |
| key.scope | object | Yes | |
| key.scope.business_id | integer or null | Yes | The one business this key may read, or null for every business the account holds. |
| key.created_at | string | Yes | |
| key.expires_at | string or null | Yes | |
| key.last_used_at | string or null | Yes | Written at most once every five minutes, so it means "used today" rather than "used at this second". |
| account | Account or null | Yes | |
| businesses | array of MeBusiness | Yes | Every business this key may read, in id order. One row when the key is scoped to one site. |
| businesses[].id | integer | Yes | |
| businesses[].name | string | Yes | |
| businesses[].city | string | Yes | |
| businesses[].country | string | Yes | |
| businesses[].language | string | Yes | |
| businesses[].plan | string or null | Yes |
MePartner
| Field | Type | Always there | What it means |
|---|---|---|---|
| kind | string | Yes | |
| key | object | Yes | The key as its holder sees it. Never the token and never its hash. |
| key.name | string | Yes | What the person who minted it called it. |
| key.prefix | string | Yes | The first twelve characters, which is enough to tell two keys apart. |
| key.abilities | array of string | Yes | |
| key.scope | object | Yes | |
| key.scope.business_id | integer or null | Yes | The one business this key may read, or null for every business the account holds. |
| key.created_at | string | Yes | |
| key.expires_at | string or null | Yes | |
| key.last_used_at | string or null | Yes | Written at most once every five minutes, so it means "used today" rather than "used at this second". |
| partner | PartnerIdentity or null | Yes | |
| connections | object | Yes | How many shops this partner has with us, by state. All three keys are always here, zeroes included, so a dashboard on its first day is not branching on a missing field. |
| connections.active | integer | Yes | |
| connections.uninstalled | integer | Yes | |
| connections.redacted | integer | Yes |
KeySummary
The key as its holder sees it. Never the token and never its hash.
| Field | Type | Always there | What it means |
|---|---|---|---|
| name | string | Yes | What the person who minted it called it. |
| prefix | string | Yes | The first twelve characters, which is enough to tell two keys apart. |
| abilities | array of string | Yes | |
| scope | object | Yes | |
| scope.business_id | integer or null | Yes | The one business this key may read, or null for every business the account holds. |
| created_at | string | Yes | |
| expires_at | string or null | Yes | |
| last_used_at | string or null | Yes | Written at most once every five minutes, so it means "used today" rather than "used at this second". |
KeyScope
| Field | Type | Always there | What it means |
|---|---|---|---|
| business_id | integer or null | Yes | The one business this key may read, or null for every business the account holds. |
Account
The account holding this key. No address: that belongs to the account and not to the machine holding a key to it.
| Field | Type | Always there | What it means |
|---|---|---|---|
| name | string | Yes | |
| locale | string | Yes | The language this account reads. English only since 2026-09-05. |
MeBusiness
The short business row /me lists. The full object, with the category and the dates, is on /businesses.
| Field | Type | Always there | What it means |
|---|---|---|---|
| id | integer | Yes | |
| name | string | Yes | |
| city | string | Yes | |
| country | string | Yes | |
| language | string | Yes | |
| plan | string or null | Yes |
PartnerIdentity
| Field | Type | Always there | What it means |
|---|---|---|---|
| name | string | Yes | |
| slug | string | Yes |
ConnectionCounts
How many shops this partner has with us, by state. All three keys are always here, zeroes included, so a dashboard on its first day is not branching on a missing field.
| Field | Type | Always there | What it means |
|---|---|---|---|
| active | integer | Yes | |
| uninstalled | integer | Yes | |
| redacted | integer | Yes |
The MCP tools
The same numbers, in the shape a language model can call. Streamable HTTP, POST only, and it takes the same key in the same header. Nothing on it writes anything: no tool changes a website, an account or a subscription, and there is nowhere in the code for one to say it does.
- Address
- https://picksyou.com/mcp
- Protocol revision
- 2025-06-18
| Tool | What it answers with |
|---|---|
| list_businesses | Every business this API key may read, with its id, name, city, country and plan. Call this first: every other tool takes one of these ids. |
| get_weeks | Week by week, how often AI assistants named this business (count) out of how many answers were sampled (n), with the usual range around that count. `rolling` is the four week window every headline is read from; a single week is never the headline. |
| get_slices | The sampled window cut one row per assistant (dimension=engine) or one row per question (dimension=prompt), each row with its count, its n, its usual range and a plain sentence saying how much weight the row will bear. |
| get_answers | Up to 10 cards from the newest sampled run: the question asked, the assistant that answered, the verdict (picked means it named this business, possible means a match nobody has confirmed yet and it counts for nothing), when it answered and the hosts it cited. The answers themselves are not carried here: the business owner reads them in their dashboard, on a proof link, or through GET /api/v1/businesses/{id}/answers with their own key. |
| get_blockers | The findings that are keeping this business out of AI answers, worst first, each with the evidence behind it and the fix. Advice only: nothing in this product changes a customer site without their own recorded approval. |
| get_named | Growth plan only. The other businesses assistants named in the same answers, with how often each was named. Counts only: an AI answer does not put businesses in order. A business that is not on Growth answers not found. |
| get_receipts | How many visitors arrived at this business website from an AI assistant, by assistant and by landing page. A count of arrivals: not a rate, not a share of traffic and never a revenue figure. |
| get_reports | Every month that has closed for this business, newest first, each with the count, the n and the two ends of its usual range. A month still running is not in this list. |
| get_report | One closed month for this business, as it was frozen at the close: the count, the n, the usual range, the assistants and the questions. Periods come from get_reports. |
The document itself
The contract this page is generated from, as a file your own tooling can read. It holds no key and no credential of ours.
https://picksyou.com/api/openapi.yaml
The written guide, from getting a key to every endpoint, travels with the source; ask us for it and we send it.
If anything on this page does not match what the API actually did, that is a bug on our side and we want to hear about it. Quote the request id from the answer you got.