An assistant that runs outside the Vendure process must obtain an OAuth token before it can act for a signed-in shopper. For a first-party assistant, you can handle the OAuth flow without showing a consent screen to the shopper. Start the flow during a navigation the shopper is already making, and your consent page can automatically approve your own client.
Use this approach when the assistant runs as a separate service from Vendure.
Use a different approach when:
Every authorization for the shop toolset redirects the browser to your oauth.storefrontConsentUrl. Even a
first-party assistant must go through the storefront OAuth flow. That redirect is where the invisibility comes from: your consent
page recognises your own client_id, approves without drawing a screen, and redirects onward. Build that page
first: the customer consent page.
To integrate your assistant, you will:
Steps 2 and 3 are browser redirects. Everything else is server-to-server.
Registration always creates a new client record with a new id. Register the first time only, and persist what you get back. Re-registering on every boot leaves a trail of unused records. It also changes your assistant's identity in the tool-call logs.
Your consent page also needs to know this client_id so it can recognise and automatically approve your
assistant.
The authorization flow requires the shopper's browser, so start it at a point where a navigation is already expected.
Every authorization needs three fresh random values. Each does a different job.
| Value | Where it goes | What it is for |
|---|---|---|
| Code verifier | Stays on your server, then goes to the token endpoint | Proves that the service redeeming the code is the one that started the flow. Never send it to the browser. |
| Code challenge | Travels in the browser, on the authorize URL | A one-way hash of the verifier. Anyone who steals it learns nothing, because they cannot work backwards to the verifier. |
| State | Travels in the browser, and comes back on the callback | Ties the callback to a flow this server started, and to the customer who started it. |
The authorization code cannot be redeemed without the code verifier.
The examples below use in-memory maps for simplicity. In a multi-instance deployment, use shared storage for pending OAuth flows and conversation state.
The token exchange gives you two tokens. For a conversation-scoped assistant, keep the tokens in memory for the lifetime of the conversation. If the assistant needs to act for a shopper after the conversation ends, persist the grant securely instead. See Act for a shopper who is not present.
The access token lasts 15 minutes by default. The refresh token lasts 30 days. Token refresh happens server-to-server, so the shopper does not need to go through OAuth again. Make sure concurrent requests share the same renewal operation.
Then call tools with whatever accessTokenFor hands back:
Sometimes the assistant must act for a shopper who is not there: following up on an abandoned basket, or answering a question later. A conversation-scoped, in-memory token is the wrong home for that. Persist the pair instead. Key it by customer identity, never by conversation.
A conversation is short-lived, and a customer may have several. If you key grants by conversation, each conversation gets its own grant and you lose access to it when the conversation ends. For persistent assistants, key the grant by customer instead so it can be refreshed, revoked, and reused across conversations.
Three things change with a persisted pair:
The state is the only thing tying a callback to a flow this backend started, and to the right customer. Skip the check and anyone can begin their own authorization, then cause a shopper's browser to complete it. Your assistant then ends up holding a token, but not the one it thinks it holds. Three obligations follow:
Never refresh the same grant concurrently.
Every refresh rotates the pair: you get a new access token and a new refresh token. Refresh-token rotation
invalidates the previous refresh token. The server treats a replay of an invalidated refresh token as evidence of
theft. If two requests refresh simultaneously, one may succeed while the other uses the now-invalid token. A
retry with that old token can cause the entire grant to be revoked. Serialise renewals, as the tokens.renewal
promise above does.
Every shopper's grant sits behind the single client record you registered above, so all of your assistant's
traffic shares one rateLimits.perClient allowance. Raise that allowance in
Set rate limits. The counters also need a cache backend that
only evicts on time, covered in
Where the rate-limit counters live.
If a call is refused, respect data.retryAfterSeconds when it is provided and avoid retrying immediately. Do
not branch on the numeric error code. The exact shape of a refusal, before and inside a tool call, is described
in What a rate-limited caller sees.
| What you see | Cause | Fix |
|---|---|---|
| The authorize call returns a 400 saying the redirect is not registered | The redirect_uri on the authorize URL is not among the redirect_uris the client was registered with. The comparison is an exact string match | Send a redirect_uri that exactly matches a registered one |
The callback carries a state with no matching flow | The flow was not started by this server, or the state was already used | Treat it as an error and store nothing |
| Callbacks intermittently find no matching flow after you scale past one instance | Pending flows are stored in one instance's memory | Store pending flows in shared storage |
The callback carries error, or no code | The shopper denied (access_denied), or the server refused (invalid_request, server_error) | There is nothing to store; start a fresh flow on a later navigation |
| The token endpoint rejects a refresh | The grant was revoked (by an administrator, your own code, or automatically after a refresh-token replay), or it expired 30 days after the last refresh | Treat the grant as dead: ask this shopper to authorize again next time they visit |
A protocol error with data.retryAfterSeconds | A rate limit refused the call before a tool ran | Wait the given seconds, and tell the shopper to try again in a moment |
Architecture reviews, custom plugin work, migrations, ongoing support. Get a hand from the team that builds Vendure.
Talk to the team