Who may act for which business
App\Modules\API\Auth\SellerScope answers it, and it is the only place that does.
That last difference is the whole reason the table exists.
The grant also says what was allowed, and that is checked per request by the
granted: middleware rather than by Passport’s scopes:. A token is issued to a
client; a permission is given by a business, and a client several businesses
connected holds one token carrying the union. Without the second check, a partner
with read-only access to one shop and write access to another could write to the
first — the token said so, and nothing asked whether that shop had agreed.
Our own app and our own staff hold no grants: they act as the person signed in, so
the token is the whole answer and granted: falls through to it.
Being staff widens which businesses you reach. It does not widen what a Connect
feed owns: a feed listing is refused to everyone.
Where clients and grants are managed
Admin → API clients
Admin → API clients
Registers an application and decides what kind it is. That is the lock in front
of every scope; registering grants nothing on its own. Revoking withdraws every
token the client issued and keeps the row: tokens already out there point at it,
and losing it would leave them unexplainable rather than refused. Only a client
already revoked can then be deleted for good, and its tokens, codes and the
sellers’ grants go with it.
Trader → Settings → Integrations
Trader → Settings → Integrations
Where a seller sees which applications may act for them and withdraws any of
them, and where they create a key for a script they run themselves. Owning that
key is the permission, so its grant row names nobody as approver.
Fields the app sees and a partner does not
App\Modules\API\Auth\InternalFields answers it for fields a partner should not even
see, keyed on the client being ours.
What sits behind it is not secret: a secret — a password, MFA material — is never
serialised at any scope, which is a stronger guarantee than a scope check. These
are things an outside integration has no business building against, because the
moment they can, the field becomes a contract we have to keep.
descriptionTranslationson a listing, and only when you name it infields[watches]. A translator maintains them after the fact; nobody writes them through the API. The website serves them to anyone, so there is nothing hidden here — only unpromised.translatedDescriptionis the one you want, and it is promised to everybody.purposeon a thread. Free text with five values in practice and null for older threads, so it is analytics rather than a contract.translatedBody,translationLocale,translationSourceLocaleandtranslationPendingon a message.
buyerEmail and buyerPhone on an order read
under a seller are in every client’s document, and null for anyone but our own app
(OrderSchema::buyerDetail()). They are a person’s contact details, which a shop’s
people may see on its dashboard and nobody agreed to hand to a partner’s system.
Add a field to that list by giving it
->hidden(fn ($request) => InternalFields::hiddenFrom($request)), not by inventing
a second definition of “is this our app”.
Why the reference describes what a partner cannot reach
The published reference documents the catalogue, the pages people read, messaging, uploads, checkout and everything under/v1/me, and its OAuth flows do not offer
their scopes. That is deliberate: a scope a
partner can read about but never obtain is honest, while one they can ask for and
are always refused reads as a bug in our API rather than in their request. And a
reference that leaves out an endpoint that exists is a reference you cannot trust.