Overview
Grocey is the write API for a person's own grocery account.
There is a catalog you can read too, but
it is the byproduct, not the product: search and stores
answer against the store someone actually shops at, cart
writes to their real basket, and, where the provider supports
it, orders returns their real order history.
Nothing here is scraped from the outside or pulled from a
partner feed; it drives the actual provider site in an
isolated browser session, signed in the same way the person
would sign in by hand.
If you just want to connect your own store to Claude or ChatGPT and you are not writing code, this page is not for you: go to Grocey for assistants instead. This page is the technical reference for builders. Grocey for developers is the shorter pitch and setup walkthrough, and your dashboard runs the real connect flow against a real account with nothing to install.
Today Grocey runs in production as a first-party substrate with
one caller: it's what powers grocery accounts inside Alfredo,
our own concierge agent. This page documents the public
developer platform being built on top of that substrate:
your own gro_* key, your own redirect, a signed
webhook when a user connects, and the same account plane
over MCP.
Two planes
The developer plane, both api.grocey.shop/v1/*
and POST /mcp, is authenticated with your
gro_* key end to end: there is no anonymous
route, including the provider directory. The end-user connect
page (link.grocey.shop/c) is a different
credential: a signed, single-use link nonce, never your key.
Holding the link gets a user their own account and no one
else's. Grocey for assistants
is that same end-user plane with a page in front of it, for
someone who would rather not read this section at all.
Authentication
Every developer-plane request will carry
Authorization: Bearer gro_live_… (or
gro_test_… against the sandbox). Keys are shown
once at creation and stored only as a hash, the same mechanism
the rest of Loki Labs' platform already uses for API keys, not
a shared secret every developer holds the same copy of.
# every developer-plane request curl https://api.grocey.shop/v1/providers?country=PL \ -H "Authorization: Bearer gro_live_…"
Connect links
Mint a link with your own redirect_url. An
https target is registered to your app ahead of
time; a custom app scheme like yourapp://return
is not, because the results come back over the API and the
webhook, so the redirect carries nothing worth stealing. Either
way it is read from the mint call and never from the page a user
lands on, which is what keeps a connect link from becoming an
open redirect on the one flow where that matters most.
The user opens the link, sees what you are asking for, picks a
provider and signs in. The browser comes back to your
redirect_url with a status, and your
original state:
-
status=connected, with aconnection_id. It worked. -
status=cancelled. The user closed the modal. No webhook fires: nothing was attempted, so calling it a failure would be wrong. -
status=failed, and only if the user chooses it. A failure shows the error and offers the return as a button. Nothing auto-redirects off a failure, because that is the one moment the explanation matters.
So treat no return at all as the same thing as
status=cancelled: a browser you do not control may
simply never come back. The webhook is what tells you a
connection exists.
POST https://api.grocey.shop/v1/connect_links { "end_user_id": "usr_42", "redirect_url": "https://your-app.example/return", "state": "whatever-you-need-back", // optional, skips the provider picker "provider": "delio" } → { "connect_link_id": "cl_9f2k…", "url": "https://link.grocey.shop/c/…", "expires_at": "2026-09-10T12:00:00Z" }
Webhooks
The redirect is a UX affordance the user can abandon; the
webhook is the record. Every payload will be signed:
Grocey-Signature: t={timestamp},v1={hex},
HMAC-SHA256 over {timestamp}.{raw body},
Stripe's scheme, because it's the one most backends already have
library code for. Delivery is at-least-once, so
event_id is the idempotency key, and ordering isn't
guaranteed across retries.
Sandbox
A gro_test_ key will reach one deterministic fixture
provider, so you can build and test a full integration before
a country you need is covered for real. The sandbox provider is
planned, not shipped. It will never appear in the coverage
table below, and it will never stand in as an oracle for
whether a real provider works.
Catalog API
Two read endpoints over the crawled catalogue, on the same
gro_* key as everything else. They are not scoped to
a connection or to an app, because a shop's shelf is the same for
everyone: what one app crawls, every app reads.
provider is required. store_id,
category_id and q narrow the result,
limit defaults to 50 and refuses anything over 200,
and paging is a cursor rather than an offset, so a crawl landing
mid-page cannot make you skip a product.
Prices are minor units plus a currency code, never a float, and
price_minor is null for an item the
store does not price on its own listing. That is a real answer,
not a missing one: reading it as zero would invent a free
product. Categories come back with their provider id and a
product count, and name is currently always
null: only a live call against a signed-in account
knows the human name of a category, and this endpoint reads D1,
not the store.
Coverage is exactly the stores Grocey crawls, listed below. A store with no crawl yet returns an empty page, not an error.
GET https://api.grocey.shop/v1/catalog/products?provider=delio&q=mleko&limit=2 → { "products": [ { "sku": "A0000709", "name": "Łaciate mleko UHT 2%", "price_minor": 399, "currency": "PLN", "available": true, "image_url": "https://images.prod.lait.app/…", "category_id": "3d2dedb1-0031-4223-aadb-65b1f256fa51", "description": null } ], // null on the last page "next_cursor": "eyJzIjoiZGVsaW8iLCJr…" } GET https://api.grocey.shop/v1/catalog/categories?provider=delio → { "categories": [ { "id": "3d2dedb1-0031-4223-aadb-65b1f256fa51", "name": null, "product_count": 1001 } ] }
Coverage
This is the one section on this page that's true right now, not a preview: generated at build time from the exact same provider declarations the API will read.
Every provider below also has its own page, generated from this same data and linked from the footer: see the full integrations index for search terms and per-store sign-in details.
Colruyt's Belgian & Luxembourg click-and-collect grocery service. Sign in with your Collect&Go e-mail and password.
Polish grocery delivery. Sign in with your phone number and an SMS code.
Polish online supermarket with van delivery. Sign in with your e-mail and password.
Żabka's express grocery delivery in Poland. Same account and sign-in as Delio.
Groceries and shops on Wolt. Sign in with the link Wolt mails to your address.
What Grocey doesn't do
Payment is off by default, and here is what would
have to be true. grocery_checkout
is a real tool on this API, not a doctrine sentence promising
one doesn't exist. Whether a call to it can ever succeed is
decided by four independent guards, enforced in code rather
than in a tool description:
- No connected provider implements it. Delio,
Frisco.pl and
Wolt each define no
checkoutmethod, so the call refuses before anything else runs. This is the guard doing the work today. -
GROCEY_CHECKOUTmust be set to exactly"enabled"in the deployment. Off by default, and only the owner can turn it on. - The person must already have a payment card saved at the provider, themselves. Grocey never collects card details, so an account with none saved cannot be checked out, flag or no flag.
- A server-minted token, bound to the exact basket at the moment it was priced, has to come back unchanged. A model can neither mint one nor edit one, and it stops verifying the instant the basket does.
None of that is a roadmap promise, and none of it is "unavailable, full stop": the mechanism is real and merged. What's missing is a provider that implements it and a switch anyone has turned on. Placing an order stays a human's tap on the provider's own site until that changes.
No favourite dishes. A provider hands back products identified by their own SKU, at their own store; nothing in that shape is a dish. A dish is an inference over a basket, something we would have to generate rather than fetch, and doing that behind an API whose whole pitch is "this is your real account" is the wrong seam: it would blur what actually came from the account against what we made up about it.
No published pricing. Cost here scales with the number of connections held open, not the number of calls made, and the concurrency ceiling that would set a sane price hasn't been measured yet. A number picked ahead of that measurement would be a guess wearing a price tag.