Beta

The Partner Trading API v1 is in beta — endpoints and response shapes may still change.

Developers
Cru World Wine
API v1 · REST & MCP
Getting started

Connected apps & OAuth

A connected app acts for one Cru customer, with their permission: an AI assistant, a portfolio tool, anything a customer signs in to with their own Cru account. It gets a token through OAuth 2.1 — the customer approves it on Cru’s site — and every trade it prepares waits for the customer to confirm.

Partners do not need this

Integrating your own business server-to-server? Use the client key and secret Cru issues you. Partner credentials →

OAuth 2.1 · connected apps. A connected app finds Cru’s authorization server, registers, sends the customer to approve it on Cru’s site, swaps the code it gets back for tokens, and calls the API as the customer.

1 / 5A call without a token gets 401, and WWW-Authenticate points to the metadata that names Cru’s authorization server.

Connecting, end to end

5 STEPS
  1. Your app

    Discover the authorization server

    GET/.well-known/oauth-authorization-server

    An unauthenticated call answers 401 with WWW-Authenticate naming the protected resource metadata, which names the authorization server and its endpoints.

    KEEPregistration_endpoint, authorization_endpoint, token_endpoint

  2. Your app

    Register your app

    POST/api/oauth/register

    Dynamic client registration is open, and grants nothing until a customer approves. Send your redirect URIs and name.

    KEEPclient_id, client_secret

  3. The customer

    The customer signs in and approves

    Send them to /api/oauth/authorize with a PKCE S256 challenge. They choose which permissions and which Cru sites your app gets.

  4. Your app

    Exchange the code for tokens

    POST/api/oauth/token

    Within 5 minutes, with the PKCE verifier. The access token lasts an hour; the refresh token 30 days, and rotates on every use.

    KEEPaccess_token, refresh_token

  5. Your app

    Call the API as the customer

    GET/api/v1/ping

    Authorization: Bearer on /api/v1 or /api/mcp, on a market host the customer allowed. Writes are staged for the customer to confirm.

What the server implements

The authorization code grant with PKCE for every client, refresh token rotation, dynamic client registration (RFC 7591), revocation (RFC 7009), server metadata (RFC 8414), protected resource metadata (RFC 9728), resource indicators (RFC 8707) and iss on the authorization response (RFC 9207). Tokens are opaque. There is no OpenID Connect, no introspection endpoint and no client credentials grant.

Each market is its own issuer

Every market hostname serves its own authorization server, and its issuer is that hostname: https://us.cruworldwine.com for the market selected above. Connect on the host whose market the customer trades in. The customer can let the app use their other Cru sites too, and the token then works on those hosts as well.

Switched on per environment

Connected apps are enabled by Cru environment by environment. Where they are not yet, the discovery documents, the OAuth endpoints and /api/mcp answer NOT_FOUND / 404, and a bearer token on /api/v1 is refused.

Discovery

A request without a token gets 401 and a WWW-Authenticate header that points at the protected resource metadata — which is how an MCP client finds its way in without being told anything but the URL.

401 · WWW-Authenticate
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="cru", resource_metadata="https://us.cruworldwine.com/.well-known/oauth-protected-resource/api/v1"
/.well-known/oauth-protected-resource/api/v1The REST API as a resource: its authorization server and scopes.
/.well-known/oauth-protected-resource/api/mcpThe MCP server, likewise.
/.well-known/oauth-authorization-server The endpoints, and what they support: code only, S256 only, authorization_code and refresh_token grants.
GET /.well-known/oauth-authorization-server
{
  "issuer": "https://us.cruworldwine.com",
  "authorization_endpoint": "https://us.cruworldwine.com/api/oauth/authorize",
  "token_endpoint": "https://us.cruworldwine.com/api/oauth/token",
  "registration_endpoint": "https://us.cruworldwine.com/api/oauth/register",
  "revocation_endpoint": "https://us.cruworldwine.com/api/oauth/revoke",
  "response_types_supported": [
    "code"
  ],
  "response_modes_supported": [
    "query"
  ],
  "grant_types_supported": [
    "authorization_code",
    "refresh_token"
  ],
  "code_challenge_methods_supported": [
    "S256"
  ],
  "token_endpoint_auth_methods_supported": [
    "none",
    "client_secret_post",
    "client_secret_basic"
  ],
  "scopes_supported": [
    "meta:read",
    "market:read",
    "wines:read",
    "portfolio:read",
    "bids:read",
    "offers:read",
    "trades:read",
    "addresses:read",
    "bids:write",
    "offers:write",
    "orders:write",
    "addresses:write"
  ],
  "authorization_response_iss_parameter_supported": true
}

Register your app

Registration is open to anyone and grants nothing on its own: a customer still has to approve the app. POST /api/oauth/register with a JSON body:

POST /api/oauth/register
curl -X POST https://us.cruworldwine.com/api/oauth/register \
  -H 'Content-Type: application/json' \
  -d '{
    "client_name": "Example Cellar Tracker",
    "redirect_uris": ["https://app.example.com/oauth/callback"],
    "token_endpoint_auth_method": "client_secret_basic"
  }'
redirect_urisrequired One to ten. https://, or http:// only on a loopback address (localhost, 127.0.0.1, [::1]), which matches on any port. An app’s own scheme such as myapp:// is allowed; javascript, data, file and similar are not. No fragments.
token_endpoint_auth_methodclient_secret_basic (the default), client_secret_post, or none for a public client with no secret. Use the one you registered, always.
grant_typesDefaults to both authorization_code and refresh_token.
client_nameUp to 100 characters, shown to the customer on the consent page.
client_uriAn https:// link to your app. Dropped if it is anything else.

The answer is 201 with a client_id (cru_oc_…) and, unless you chose none, a client_secret (cru_cs_…) that does not expire and is shown only this once.

Send the customer to approve it

Authorization request
https://us.cruworldwine.com/api/oauth/authorize
  ?response_type=code
  &client_id=cru_oc_YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
  &scope=meta%3Aread%20market%3Aread%20portfolio%3Aread%20bids%3Aread%20bids%3Awrite
  &code_challenge=YOUR_S256_CHALLENGE
  &code_challenge_method=S256
  &state=YOUR_STATE
code_challenge, code_challenge_method=S256 Required, for every client. The challenge is the base64url SHA-256 of a 43–128 character verifier you keep for the token request. plain is refused.
redirect_uriExactly as registered. It may be left out only if you registered just one.
scope Space-separated. Ask for nothing and you get the read scopes but addresses:read, which you must ask for by name. offline_access is accepted and ignored; scopes Cru does not offer apps are dropped. The scopes →
resource Optional: https://us.cruworldwine.com/api/v1 or https://us.cruworldwine.com/api/mcp. A token issued for one works only there; a token issued without one works on both.
stateUp to 2048 characters, returned unchanged.

The customer signs in to their Cru account if they are not already, on that market’s site. The consent page names your app — by the client_name you registered, which Cru cannot verify — and the address it will return them to, and asks two things:

  • What it can see and do. The permissions you asked for, in two groups. The read permissions start ticked; meta:read cannot be unticked. The trading permissions and addresses:write start unticked even when you asked for them, with the promise that each bid, sale, purchase or address is shown to the customer first and only happens when it is separately confirmed.
  • Which Cru sites it can use. The current one always, others if they tick them.

The customer can only take permissions away from what you asked for, never add to it. If they decline — or allow with nothing ticked — you get error=access_denied. The page stays answerable for 15 minutes. An account on hold cannot connect apps at all.

The redirect back

On approval the customer is sent to your redirect_uri with code, state and iss. Check iss is the issuer you started with. The code is single use and lasts five minutes.

Exchange the code

POST /api/oauth/token
curl -X POST https://us.cruworldwine.com/api/oauth/token \
  -u "$CRU_OAUTH_CLIENT_ID:$CRU_OAUTH_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code=cru_ac_THE_CODE \
  -d code_verifier=YOUR_PKCE_VERIFIER \
  -d redirect_uri=https://app.example.com/oauth/callback
200 · Token response
{
  "access_token": "cru_at_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "meta:read market:read portfolio:read bids:read bids:write",
  "refresh_token": "cru_rt_…"
}
Access tokencru_at_…One hour. Send it as Authorization: Bearer.
Refresh tokencru_rt_… 30 days, and rotated: every refresh returns a new pair and spends the old refresh token.
Authorization codecru_ac_…Five minutes, once.

Refresh

POST /api/oauth/token
curl -X POST https://us.cruworldwine.com/api/oauth/token \
  -u "$CRU_OAUTH_CLIENT_ID:$CRU_OAUTH_CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d refresh_token=cru_rt_THE_REFRESH_TOKEN

A refresh may narrow the scopes, never widen them. Use each refresh token once: presenting one that has already been used revokes the whole connection — every token issued since the customer approved — on the assumption that it was stolen. So does replaying an authorization code. If the customer’s account goes on hold, the next refresh ends the connection for good.

Disconnect

POST /api/oauth/revoke with token= and your client authentication ends the whole connection, whichever of the two tokens you send. It always answers 200 {}, so it cannot be used to test whether a token exists. Customers cannot yet disconnect an app from their Cru account themselves: they ask Cru, which can also suspend an app outright.

Calling the API with a token

The REST API and the MCP server accept the token on any market host the customer allowed. It acts as that customer, with the scopes they granted:

  • Reads behave exactly as for a partner. GET /ping shows the app’s name, the scopes, the market you called and when the token expires.
  • Writes are staged and answer 202; nothing happens until you confirm the code with the customer’s agreement. Writes & confirmations →
  • An expired, revoked or wrong-resource token is AUTH_INVALID_CREDENTIALS / 401, with error="invalid_token" in WWW-Authenticate. A missing scope adds error="insufficient_scope" and the scope it needs.
  • A host the customer did not allow is AUTH_STORE_NOT_ALLOWED; an account on hold, AUTH_ACCOUNT_ON_HOLD.

OAuth errors

The OAuth endpoints answer in OAuth’s own format, {"error", "error_description"}, not the API envelope. Before the client and redirect are known to be valid, the authorization page shows the customer an error instead of redirecting.

invalid_request400A parameter is missing or malformed — most often a missing PKCE challenge or verifier. 403 when the request was not HTTPS.
invalid_client401Unknown or suspended client, wrong secret, or a client authenticating differently from how it registered.
invalid_grant400The code or refresh token is invalid, expired, already used or revoked; the redirect or PKCE verifier does not match; or the account can no longer connect apps.
invalid_scope400Nothing requested is grantable, or a refresh asked for more than was approved.
invalid_target400resource is not /api/v1 or /api/mcp on this host.
access_deniedredirectThe customer declined, ticked nothing, or has an account that cannot connect apps.
unauthorized_client400The client did not register the grant it is using.
unsupported_grant_type400grant_type is not authorization_code or refresh_token.
unsupported_response_typeredirectresponse_type is not code.
invalid_redirect_uri400Registration: a redirect URI breaks the rules above.
invalid_client_metadata400Registration: the body is not a JSON object, or a field is invalid.
server_error500Cru’s to fix.
Cru World Wine Developers · Partner Trading API v1
Based on the API as built on 15 October 2026.

API credentials

Enter the client key and secret issued by Cru. They are kept for this browser tab’s session — a reload keeps them, closing the tab or clearing them ends them.

Credentials are never included in copied examples, and are redacted from anything the console prints back.

Request API access

Tell us who you are and what you want to build. Cru issues a client key and secret for your organisation, scoped to what you need.

Markets you trade in (choose one or more)

We use what you send here to reply about API access, and for nothing else. Cloudflare Turnstile checks the form for bots (privacy policy).