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.
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_existsand nothing is created. A second account for one kitchen would split its balance, its console and its history in two. That409is the way into this page.
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/callbackdoes not also allowhttps://partner.example.com/listo/callback/done. - It must be
https://, except onlocalhostand127.0.0.1, wherehttp://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_failedandparam: "redirect_uri". It fails on your own server, before any restaurant is shown anything.
Ask for the link
POST /v1/connect_requests
takes the owner’s address, one of your registered redirect URIs, and optionally a
state of your own:
201 with a link and a deadline:
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
What the owner sees
Send the owner’s browser tourl, 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
The browser comes back
When the owner decides, Listo sends their browser to yourredirect_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, astore.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:
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
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"andbilling: "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
planblock 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 ownstore.connected, and all of them are onGET /v1/storesfrom 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 asTortería La Mera Mera Centro, is published as it is. - It has no
external_id, because you never sent one. Set one withPATCH /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.