Skip to main content
Most stores you add are new to Listo, and you create them with POST /v1/stores. Some restaurants already run on Listo with an account of their own: their own console, their own balance, their own delivery history. You do not create a store for one of those. You ask its owner to connect the account they already have, they approve it on a Listo page, and you get the store. Nothing is created along the way. What you end up holding is an ordinary store, and every other endpoint treats it exactly like one you created.
Live keys onlyThis flow works with a live key and nothing else. A test key gets 404 not_found from both endpoints on this page, because there is no test version of a real restaurant’s real account. What you rehearse in test mode is POST /v1/stores, which is what runs whenever the owner has no account of their own.

Create it, or connect it

owner_email on POST /v1/stores is how you find out which of the two you are doing. It is a check rather than a field of the store: send the address of whoever runs the location, and before anything is written Listo checks whether that address already signs in to a restaurant’s own Listo account.
  • There is none. The store is created, and the path from there is the usual one: set its access_tier (see the money model) and, if somebody at the restaurant is to sign in, give them a login.
  • There is one. The create answers 409 account_exists and nothing is created. A second account for one kitchen would split its balance, its console and its history in two. That 409 is the way into this page.
Nothing stores the address and no store object echoes it back. It only decides which of the two paths you take.

Register your redirect URI first

When the owner decides, Listo sends their browser back to you, and only ever to an address you registered. Add your callback on the Redirect URIs page of the partner dashboard before your first request.
  • The whole string is compared. Register the full callback URL, not only the origin. There are no wildcards and no prefixes, so https://partner.example.com/listo/callback does not also allow https://partner.example.com/listo/callback/done.
  • It must be https://, except on localhost and 127.0.0.1, where http:// is allowed so you can build against your own machine.
  • The list belongs to your company, not to a mode, so it does not change when you switch between live and test in the dashboard. Register your production callback and the one you develop against.
  • An address that is not on the list is refused when you ask for the link, with 422 validation_failed and param: "redirect_uri". It fails on your own server, before any restaurant is shown anything.
POST /v1/connect_requests takes the owner’s address, one of your registered redirect URIs, and optionally a state of your own:
It answers 201 with a link and a deadline:
That is everything it tells you. Listo works out which restaurant the address belongs to and keeps that to itself: you learn which account you connected only if the owner approves. The link is good for 30 minutes from when you ask for it. Minting another is free, so an expired link is a button to render again, not an error to handle. Ask for it when the owner is about to use it rather than hours ahead. state is yours: any value up to 255 characters, which Listo never reads and sends back untouched. It is how you tie the browser that comes back to the session that left.
connect_request.rb
The link carries a token that opens the owner’s consent page, which is why the sample prints only its host. Send it to the owner and nowhere else, and keep it out of your logs.

What the owner sees

Send the owner’s browser to url, with a redirect, a link or a button in your product. Everything from there happens on Listo. This is what they read, in the words on their screen, so that you and the restaurant are looking at the same sentences if they call you. Signing in. Unless that browser is already signed in to their Listo console, they land on a page titled “Connect your account”, which reads “Sign in to connect their restaurant with your company”. They type the email and password they already use for Listo and press Sign in and continue. There is no sign-up here: the page’s only other way out is “No Listo account? Go back to your company”, which brings them back to you with error=access_denied. The consent screen. It is titled “Your company wants to connect to your account” and reads “Authorize the connection so your company can send and manage deliveries with your Listo account.” It shows their restaurant and the address they signed in as, then what they are granting, under “When you connect, your company will be able to:”
  • Create deliveries on behalf of your business
  • See your deliveries and their status
  • Check your delivery balance
It closes with “Your deliveries are still paid from your Listo balance, just as they are today.” and two buttons, Allow connection and Cancel. When the restaurant has more than one branch, the consent screen also lists every branch that will connect, because approving connects all of them at once (see what a connected store is). Both names on those pages come from Listo, never from your request: your company is your company’s name as Listo has it on your partner account, and their restaurant is the name of the restaurant’s business in Listo, the account as a whole rather than one of its branches. No field on this API puts words on those pages. Only an owner of the account can approve, and it does not have to be the person whose address you sent: that address is only how Listo found the account. A member of staff who signs in is told “Only an owner can authorize this connection.” A link that expired or was cancelled opens a page titled “This request is no longer valid”; a link the owner already approved opens one titled “This connection is already approved”. Both have a button back to you, and that button brings them back to you with the outcome that link already had.

The browser comes back

When the owner decides, Listo sends their browser to your redirect_uri: error=access_denied means nothing was connected and that link is finished, whether the owner pressed Cancel on the consent screen, left from the sign-in page, or opened a link that was no longer valid. It cannot be reopened, so if the owner changes their mind, ask for a new one. If the owner reopens a link they already approved, their browser comes back to you with the same code again; exchanging it twice is safe. Listo does not email the owner about it either way. If you want to confirm the connection to them, that message is yours to send. state comes back exactly as you sent it, on both. Check it against the value you stored for that session before you act on the redirect, and ignore one whose state you do not recognise: it is your protection against a forged callback. If you sent no state, none comes back. If the URI you registered already carries a query string of its own, these parameters are added to it.

The webhook does not wait for the browser

When the owner approves, a store.connected event goes to your live webhook endpoint, and data is the whole store object, id included. It does not depend on the owner’s browser, so it can reach you before the redirect does, and it still reaches you when the redirect never does: an owner can approve on a phone and close the tab before their browser gets back to you. Treat the store as connected when either one reaches you, whichever is first, and deduplicate on the event id as with every event. Setting up the endpoint is receiving webhooks.

Exchange the code

POST /v1/connections takes the code from the redirect:
It answers 201 with the store object, exactly the shape POST /v1/stores returns, st_... id included. From then on it is an ordinary store on every other endpoint. The exchange answers with one store, and a restaurant with several branches arrives as several stores. The others come as their own store.connected events and are on GET /v1/stores, so take your list of connected stores from those, never from the exchange alone.
exchange_connection.rb
The code is good for 24 hours from the approval. That is far longer than the link on purpose: what the code has to survive is a call between two servers, which may need retrying through an outage on either side. Past that it is 400 code_expired, but the connection itself happened when the owner approved: the store is on GET /v1/stores and its store.connected was sent then, so read it from there. Do not start a new connect request for it, because the account is already connected. Exchanging a code a second time returns the same store again. There is no “already used” refusal, so an exchange whose response never reached you is simply repeated. Only your key can exchange your codes: any other key gets 404 not_found for one.

What a connected store is

  • It is on access_tier: "full" and billing: "store". The restaurant keeps its whole console, its balance and its history, and the deliveries you create for it are paid from its own balance, as its consent screen said. Orders it dispatches from its own console reach you like any other; see where a delivery came from.
  • From the restaurant’s next renewal, its Plan Listo is charged to you. Until then it keeps paying its own plan, and nothing is charged twice. If it had no live plan, the plan starts the day it connects and that month is charged to you that day. If its plan is unpaid when it connects, your plan starts that day too: the month it had not paid is voided, so nobody is charged for it, and one full month is charged to you that day. The store’s plan block says which it is; see plan and billing. If your agreement has each restaurant pay its own plan, none of this applies: the restaurant keeps paying its own plan, nothing moves to you and nothing is charged to you; see where each restaurant pays its own plan.
  • Every branch connects at once. The owner consents for the account, not for one location, and nothing in the flow asks about branches. A restaurant with three branches becomes three stores, each with its own st_... id: the exchange answers with one of them, each of the three sends its own store.connected, and all of them are on GET /v1/stores from that moment. One exchange does not mean one store. Each of them is named after the business and the branch, Tortería La Mera Mera, Zapopan, so your list still reads as one restaurant. A branch whose own name already begins with the business’s name, such as Tortería La Mera Mera Centro, is published as it is.
  • It has no external_id, because you never sent one. Set one with PATCH /v1/stores/{id} if you key your stores on your own ids.
  • Connecting is not a transfer. An account that is already connected, to you or to anybody else, cannot be connected again, and this API has no way to move a store from one partner to another. That is deliberate: the restaurant whose balance pays would have to agree to it.

When it is refused

The two owner_email refusals share a code, and error.message tells them apart for whoever reads your logs. Branch on the code, and show the message to the person setting up the connection.