Beta

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

Developers
Cru World Wine
API v1 · REST & MCP
Developer tools

SDKs & code examples

Cru publishes SDKs for PHP, Go and Python, and a Postman collection, all generated from one OpenAPI description of the v1 API. Every endpoint is a typed method and every answer a typed model. Each SDK also:

  • keeps working when Cru adds a field, or a value to a field;
  • retries a read, or a partner credential’s write, after a timeout or a passing error, sending the same reference — made for you if you give none — so a write never acts twice;
  • connects with a partner credential’s key and secret, or a connected app’s access token, which it refreshes once when it expires;
  • gives a connected app’s write back staged, with the preview to show the customer, and confirms it on their yes;
  • pages through limit and offset for you.

The three are open source, under the Apache 2.0 licence. Without one, every endpoint page has plain PHP and Python beside it. Calling the API without an SDK →

Set your environment variables

Environment
export CRU_BASE_URL='https://us.cruworldwine.com/api/v1'
export CRU_CLIENT_KEY='YOUR_CLIENT_KEY'
export CRU_CLIENT_SECRET='YOUR_CLIENT_SECRET'

Use the API hostname for your environment and market. Keep the credentials in server environment variables or your application’s secret manager. A connected app uses CRU_ACCESS_TOKEN instead of the key and secret. Each SDK reads these when given nothing, and CRU_MARKET too: the market to call when CRU_BASE_URL is not set.

PHP SDK

PHP 8.1 or later, and Guzzle 7.5 or later.

Terminal
composer require cruworldwine/cru-api-php
PHP · partner credential
use CruWorldWine\Client;

$cru = new Client(clientKey: 'cru_live_...', clientSecret: $secret, market: 'uk');
$client = $cru->ping()->client;

foreach ($cru->market->listBids(['lwin' => ['123456720150600750'], 'limit' => 50])->autoPagingIterator() as $wine) {
    foreach ($wine->bids ?? [] as $bid) {
        echo $wine->wine_name, ': ', $bid->quantity, ' at ', $bid->price, ' ', $wine->currency, "\n";
    }
}
$write = $cru->bids->place(['lwin18' => '123456720150600750', 'quantity' => 6, 'price' => 1200.50]);
PHP · connected app
$app = new Client(accessToken: $tokens['access_token']);

Go SDK

Go 1.23 or later, and nothing beyond the standard library.

Terminal
go get github.com/cruworldwine/cru-api-go/cru@main

This installs from GitHub until the first release. After it, go get github.com/cruworldwine/cru-api-go/cru does the same.

Go · partner credential
import "github.com/cruworldwine/cru-api-go/cru"

client, err := cru.NewClient(cru.WithClientKey("cru_live_...", secret), cru.WithMarket("uk"))
if err != nil {
	log.Fatal(err)
}
p, err := client.Ping(ctx)
fmt.Println(p.Client.Market, p.Client.Currency, p.Client.Scopes)
Go · connected app
client, err := cru.NewClient(
	cru.WithAccessToken(accessToken),
	cru.WithOAuthRefresh(refreshToken, clientID, clientSecret, func(t cru.Token) {
		save(t.AccessToken, t.RefreshToken)
	}),
)

Python SDK

Python 3.9 or later, with Cru for plain code and AsyncCru for asyncio.

Terminal
pip install git+https://github.com/cruworldwine/cru-api-python

This installs from GitHub until the first release. After it, pip install cruworldwine does the same.

Python · partner credential
from cruworldwine import Cru

with Cru(client_key='cru_live_...', client_secret='...', market='uk') as cru:
    client = cru.ping().client
    print(client.organisation, client.market, client.currency, client.scopes)
    for wine in cru.portfolio.list(is_listed=True).auto_paging_iter():
        for line in wine.portfolio:
            print(wine.wine_name, line.quantity, line.pricing.listing_price, line.pricing.net_proceeds)
Python · connected app
cru = Cru(
    access_token=tokens['access_token'],
    refresh_token=tokens['refresh_token'],
    oauth_client_id='your-client-id',
    oauth_client_secret=None,  # only for a confidential client
    on_token_refresh=save_tokens,
)

Postman collection

Cru Trading API v1

Every endpoint, in folders by area · Postman collection format 2.1

Download
  1. Download the file in your browser. Postman’s Import from a link cannot fetch it from this site.
  2. In Postman, choose Import and drop the file in.
  3. In the collection’s variables, set clientKey and clientSecret, or a connected app’s accessToken, and the market to call.
  4. Run a folder top to bottom: each list saves the codes and ids the requests after it use, and a staged write saves its confirmation code.
The writes are real

A partner credential’s writes run at once, so running a folder of them places, changes and cancels a real bid, or lists real wine. Read freely, and send a write only when you mean it.

To import by link instead, into Postman or an OpenAPI code generator, use the OpenAPI description the SDKs are generated from: openapi.yaml .

Without an SDK

Every endpoint page has a PHP and a Python example beside it, built from the request on screen: plain PHP with the cURL extension, and Python with requests. Neither needs an SDK, and both follow the same v1 contract.

Make your first request

PHP · cURL
<?php

$curl = curl_init(getenv('CRU_BASE_URL') . '/ping');
curl_setopt_array($curl, [
    CURLOPT_HTTPHEADER => [
        'CLIENT-KEY: ' . getenv('CRU_CLIENT_KEY'),
        'CLIENT-SECRET: ' . getenv('CRU_CLIENT_SECRET'),
        'Accept: application/json',
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => false,
    CURLOPT_TIMEOUT => 30,
]);

$payload = json_decode(curl_exec($curl), true);
print_r($payload['data']['client']);
Python · requests
import os
import requests

response = requests.get(
    os.environ["CRU_BASE_URL"] + "/ping",
    headers={
        "CLIENT-KEY": os.environ["CRU_CLIENT_KEY"],
        "CLIENT-SECRET": os.environ["CRU_CLIENT_SECRET"],
        "Accept": "application/json",
    },
    timeout=30,
)
print(response.json()["data"]["client"])

Read every page

Advance offset by limit until meta.pagination.has_more is false. On the books a page is wines, each with its bids or offers.

Python
import os
import requests

session = requests.Session()
session.headers.update({
    "CLIENT-KEY": os.environ["CRU_CLIENT_KEY"],
    "CLIENT-SECRET": os.environ["CRU_CLIENT_SECRET"],
    "Accept": "application/json",
})

offset = 0
while True:
    page = session.get(
        os.environ["CRU_BASE_URL"] + "/market/bids",
        params={"limit": 250, "offset": offset},
        timeout=30,
    ).json()
    for market in page["data"]["markets"]:
        print(market["lwin18"], market["wine_name"], len(market["bids"]))
    pagination = page["meta"]["pagination"]
    if not pagination["has_more"]:
        break
    offset += pagination["limit"]

Retry a write safely

Generate the reference once, store it, and send the same request again if the first gets no answer. Cru replays the first outcome rather than acting twice.

Python
import os
import uuid
import requests

# Generate once, and store it with your own record of the order.
reference = "bid-" + uuid.uuid4().hex[:24]

def place_bid():
    return requests.post(
        os.environ["CRU_BASE_URL"] + "/bids",
        headers={
            "CLIENT-KEY": os.environ["CRU_CLIENT_KEY"],
            "CLIENT-SECRET": os.environ["CRU_CLIENT_SECRET"],
            "Accept": "application/json",
        },
        json={
            "reference": reference,
            "lwin18": "101187220240600750",
            "quantity": 1,
            "price": 1650.50,
        },
        timeout=30,
    )

try:
    response = place_bid()
except requests.Timeout:
    # The same reference: placed at most once, however many times this runs.
    response = place_bid()

print(response.status_code, response.headers.get("Idempotent-Replayed"))

Handle API errors

PHP
$payload = json_decode($body, true);
if (($payload['status'] ?? null) !== 'OK') {
    $error = $payload['errors'][0] ?? [];
    // Log metadata only. Never log your secret.
    error_log(sprintf('%d %s %s %s',
        curl_getinfo($curl, CURLINFO_RESPONSE_CODE),
        $error['code'] ?? 'UNKNOWN',
        $error['field'] ?? '-',
        $payload['meta']['request_id'] ?? '-'));
}

Prefer Python?

Install requests, set your credentials, then choose Python in an endpoint’s request panel.

Terminal
python -m pip install requests
Explore Python examples
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).