# Quickstart (https://eulabel.eu/docs/documentation/get-started/quickstart)

> Fetch the complete documentation index at: https://eulabel.eu/docs/llms.txt
> Use this file to discover all available pages before exploring further.
> Full content: https://eulabel.eu/docs/llms-full.txt
> Append .md to any page URL for markdown, or send Accept: text/markdown.



This guide walks you through creating a wine Digital Product Passport, retrieving it, and downloading the QR code -- all with simple API calls. One call creates the product and passport together.

> **Note**
> **Wine-only passports** — passport data currently requires `productType: "wine"`. Support for textile, food, electronics, and battery passports is on the roadmap. Submitting identical passport data for the same product returns the existing passport (HTTP 200) instead of creating a duplicate (HTTP 201).

Prerequisites [#prerequisites]

* An EUlabel account with an API key (`sk_test_...` for sandbox, `sk_live_...` for production)
* `curl` or any HTTP client

> **Note**
> For the fastest start, use the sandbox first. You can migrate the same workflow to production by switching the base URL and API key.

Create a Digital Product Passport [#create-a-digital-product-passport]

    Send the product identity (GTIN, name, brand, category) and passport data in a single call. If the GTIN is new, EUlabel creates the product automatically.

    
### CURL

```bash
export EULABEL_API_KEY="sk_test_..."

curl -X POST https://api.eulabel.eu/v1/passports \
  -H "Authorization: Bearer $EULABEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "gtin": "5601234567890",
    "name": "Quinta do Crasto Douro Red 2021",
    "brand": "Quinta do Crasto",
    "category": "wine",
    "data": {
      "productType": "wine",
      "colour": "red",
      "vintage": 2021,
      "ingredients": ["Grapes (Touriga Nacional, Touriga Franca)", "Sulphur Dioxide"],
      "nutrition": {
        "energyKj": 351, "energyKcal": 84,
        "fatG": 0, "saturatedFatG": 0,
        "carbohydratesG": 0.6, "sugarsG": 0.3,
        "proteinG": 0.1, "saltG": 0, "alcoholG": 13.5
      },
      "allergens": {
        "containsSulphites": true,
        "containsEgg": false,
        "containsFish": false,
        "containsMilk": false
      },
      "origin": { "country": "PT", "region": "Douro", "designation": "Douro DOC" },
      "producers": [
        { "name": "Quinta do Crasto", "role": "producer" }
      ]
    }
  }'
```
### JavaScript

```javascript
const EULABEL_API_KEY = process.env.EULABEL_API_KEY;

const response = await fetch('https://api.eulabel.eu/v1/passports', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${EULABEL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    gtin: '5601234567890',
    name: 'Quinta do Crasto Douro Red 2021',
    brand: 'Quinta do Crasto',
    category: 'wine',
    data: {
      productType: 'wine',
      colour: 'red',
      vintage: 2021,
      ingredients: ['Grapes (Touriga Nacional, Touriga Franca)', 'Sulphur Dioxide'],
      nutrition: {
        energyKj: 351, energyKcal: 84,
        fatG: 0, saturatedFatG: 0,
        carbohydratesG: 0.6, sugarsG: 0.3,
        proteinG: 0.1, saltG: 0, alcoholG: 13.5,
      },
      allergens: {
        containsSulphites: true, containsEgg: false,
        containsFish: false, containsMilk: false,
      },
      origin: { country: 'PT', region: 'Douro', designation: 'Douro DOC' },
      producers: [{ name: 'Quinta do Crasto', role: 'producer' }],
    },
  }),
});

if (!response.ok) {
  const error = await response.json();
  throw new Error(`${error.error.code}: ${error.error.message}`);
}

const passport = await response.json();
console.log(passport.id);          // "ps_w9x2kf"
console.log(passport.digitalLink); // "https://eulabel.eu/01/05601234567890"
console.log(passport.status);      // "published"
```
### Python

```python
import requests

EULABEL_API_KEY = "sk_test_..."

response = requests.post(
    "https://api.eulabel.eu/v1/passports",
    headers={"Authorization": f"Bearer {EULABEL_API_KEY}"},
    json={
        "gtin": "5601234567890",
        "name": "Quinta do Crasto Douro Red 2021",
        "brand": "Quinta do Crasto",
        "category": "wine",
        "data": {
            "productType": "wine",
            "colour": "red",
            "vintage": 2021,
            "ingredients": ["Grapes (Touriga Nacional, Touriga Franca)", "Sulphur Dioxide"],
            "nutrition": {
                "energyKj": 351, "energyKcal": 84,
                "fatG": 0, "saturatedFatG": 0,
                "carbohydratesG": 0.6, "sugarsG": 0.3,
                "proteinG": 0.1, "saltG": 0, "alcoholG": 13.5,
            },
            "allergens": {
                "containsSulphites": True, "containsEgg": False,
                "containsFish": False, "containsMilk": False,
            },
            "origin": {"country": "PT", "region": "Douro", "designation": "Douro DOC"},
            "producers": [{"name": "Quinta do Crasto", "role": "producer"}],
        },
    },
)
response.raise_for_status()

passport = response.json()
print(passport["id"])           # "ps_w9x2kf"
print(passport["digitalLink"])  # "https://eulabel.eu/01/05601234567890"
print(passport["status"])       # "published"
```
Response:
```json
{
  "id": "ps_w9x2kf",
  "gtin": "05601234567890",
  "digitalLink": "https://eulabel.eu/01/05601234567890",
  "qrCode": "https://eulabel.eu/qr/ps_w9x2kf.svg",
  "status": "published",
  "version": 1,
  "createdAt": "2026-03-25T10:00:00Z",
  "updatedAt": "2026-03-25T10:00:00Z",
  "compliance": {
    "status": "compliant",
    "regulation": "EU Wine e-label Regulation (Reg. 2021/2117)",
    "completeness": 100,
    "missingFields": [],
    "documentationUrl": "https://eulabel.eu/docs/api-reference/passports#compliance"
  }
}
```
    The `compliance.status` is `"compliant"` because all required fields are present. If you send partial data, the status will be `"draft"` and `missingFields` will tell you exactly what is needed.
Progressive enrichment (optional) [#progressive-enrichment-optional]

    You do not need to send all data at once. Send what you have, then enrich later. The API merges new fields into the existing passport.
```bash
# First call -- just ingredients
curl -X POST https://api.eulabel.eu/v1/passports \
  -H "Authorization: Bearer $EULABEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "gtin": "5601234567891",
    "name": "Another Wine",
    "brand": "Brand",
    "category": "wine",
    "data": {
      "productType": "wine",
      "ingredients": ["Grapes", "Sulphites"]
    }
  }'
# Response: status "draft", compliance.completeness 20%

# Second call -- add nutrition (merged with existing data)
curl -X POST https://api.eulabel.eu/v1/passports \
  -H "Authorization: Bearer $EULABEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "gtin": "5601234567891",
    "data": {
      "nutrition": { "energyKj": 300, "energyKcal": 72, "fatG": 0, "saturatedFatG": 0, "carbohydratesG": 0.5, "sugarsG": 0.2, "proteinG": 0.1, "saltG": 0, "alcoholG": 12 }
    }
  }'
# Response: status "draft", compliance.completeness 40%
```
    Each call returns an updated `compliance` object that tells you which fields are still missing.
Retrieve the passport [#retrieve-the-passport]

    
### CURL

```bash
curl https://api.eulabel.eu/v1/products/a1b2c3d4-.../passport \
  -H "Authorization: Bearer $EULABEL_API_KEY"
```
### JavaScript

```javascript
const passportGet = await fetch(
  'https://api.eulabel.eu/v1/products/a1b2c3d4-.../passport',
  { headers: { 'Authorization': `Bearer ${EULABEL_API_KEY}` } }
);

if (!passportGet.ok) {
  const error = await passportGet.json();
  throw new Error(`${error.error.code}: ${error.error.message}`);
}

const passportData = await passportGet.json();
```
### Python

```python
passport_resp = requests.get(
    "https://api.eulabel.eu/v1/products/a1b2c3d4-.../passport",
    headers={"Authorization": f"Bearer {EULABEL_API_KEY}"},
)
passport_resp.raise_for_status()
passport_data = passport_resp.json()
```
The response contains the full passport data, compliance status, GS1 Digital Link, and QR code URL.
Download the QR code [#download-the-qr-code]

    
### CURL

```bash
curl https://api.eulabel.eu/v1/products/a1b2c3d4-.../qr \
  -H "Authorization: Bearer $EULABEL_API_KEY" \
  -o label-qr.svg
```
### JavaScript

```javascript
const qrResponse = await fetch(
  'https://api.eulabel.eu/v1/products/a1b2c3d4-.../qr',
  { headers: { 'Authorization': `Bearer ${EULABEL_API_KEY}` } }
);

if (!qrResponse.ok) {
  throw new Error(`QR code download failed: ${qrResponse.status}`);
}

const svg = await qrResponse.text();
```
### Python

```python
qr = requests.get(
    "https://api.eulabel.eu/v1/products/a1b2c3d4-.../qr",
    headers={"Authorization": f"Bearer {EULABEL_API_KEY}"},
)
qr.raise_for_status()

with open("label-qr.svg", "wb") as f:
    f.write(qr.content)
```
This generates an SVG QR code encoding a GS1 Digital Link URI. When scanned, the resolver routes users to the appropriate passport view.
What happens when the QR code is scanned [#what-happens-when-the-qr-code-is-scanned]

 B[eulabel.eu/01/05601234567890]:::blue
    B --> C[Resolver detects audience context]:::blue
    C --> D[Passport page displayed with ingredients, nutrition, allergens]:::green"
/>

The resolver serves different data to different audiences -- consumers see the product story, regulators get structured compliance data, and recyclers see material composition.

Next steps [#next-steps]

- [API Keys](https://eulabel.eu/docs/documentation/get-started/api-keys) — Create, rotate, and revoke credentials safely
- [Sandbox](https://eulabel.eu/docs/documentation/get-started/sandbox) — Test with pre-populated sample data
- [API Reference](https://eulabel.eu/docs/api-reference) — Every endpoint with request/response examples
- [SDKs](https://eulabel.eu/docs/documentation/sdks) — Use the TypeScript or Python SDK instead of raw HTTP

