> ## Documentation Index
> Fetch the complete documentation index at: https://code.dcycle.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Bulk Upload Purchases

> Get a presigned URL to upload multiple purchases via CSV

<Warning>
  **Legacy API.** This page documents the previous version of the API. This particular
  operation has no documented equivalent in the current API yet, so this remains the
  documented way to do it. Prefer the current API for everything it already covers.
</Warning>

Upload hundreds of purchases at once via a CSV file. This endpoint returns a presigned S3 URL so you can upload your file, which will be processed asynchronously.

<Note>
  Purchases uploaded this way are persisted in the database and emissions are calculated automatically based on economic input-output factors.
</Note>

## Upload Flow

<Steps>
  <Step title="Request presigned URL">
    Make a POST request to `/api/v1/purchases/bulk/csv` with your file name
  </Step>

  <Step title="Upload to S3">
    Use the presigned URL to upload your CSV directly to S3 (PUT request)
  </Step>

  <Step title="Asynchronous processing">
    The system processes your file in the background, creating purchases and calculating emissions
  </Step>

  <Step title="Verify results">
    Check your purchases with the [List Purchases](/api-docs/purchases/list) endpoint
  </Step>
</Steps>

## Step 1: Request Presigned URL

## Request

### Headers

<ParamField header="x-api-key" type="string" required>
  Your API key for authentication

  **Example:** `sk_live_1234567890abcdef`
</ParamField>

<ParamField header="x-organization-id" type="string" required>
  Your organization UUID

  **Example:** `ff4adcc7-8172-45fe-9cf1-e90a6de53aa9`
</ParamField>

<ParamField header="x-user-id" type="string" required>
  Your user UUID

  **Example:** `a1b2c3d4-e5f6-7890-abcd-ef1234567890`
</ParamField>

### Body Parameters

<ParamField body="file_name" type="string" required>
  Name of the CSV file you're going to upload

  **Example:** `"2024_q1_purchases.csv"`
</ParamField>

<ParamField body="content_type" type="string">
  MIME type of the file (defaults to CSV)

  **Example:** `"text/csv"`
</ParamField>

### Response

<ResponseField name="upload_url" type="string">
  Presigned S3 URL to upload the purchases file
</ResponseField>

<ResponseField name="file_name" type="string">
  Sanitized file name (alphanumeric only)
</ResponseField>

<ResponseField name="file_id" type="string">
  File UUID for tracking
</ResponseField>

<ResponseField name="destination_file_key" type="string">
  Full S3 key where file will be stored
</ResponseField>

<ResponseField name="message" type="string">
  Confirmation message
</ResponseField>

### Example

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST "https://api.dcycle.io/api/v1/purchases/bulk/csv" \
    -H "Authorization: Bearer ${DCYCLE_API_KEY}" \
    -H "x-organization-id: ${DCYCLE_ORG_ID}" \
    -H "x-user-id: ${DCYCLE_USER_ID}" \
    -H "Content-Type: application/json" \
    -d '{
      "file_name": "2024_q1_purchases.csv"
    }'
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests
  import os

  api_key = os.getenv("DCYCLE_API_KEY")
  org_id = os.getenv("DCYCLE_ORG_ID")
  user_id = os.getenv("DCYCLE_USER_ID")

  headers = {
      "Authorization": f"Bearer {api_key}",
      "x-organization-id": org_id,
      "x-user-id": user_id,
      "Content-Type": "application/json"
  }

  payload = {
      "file_name": "2024_q1_purchases.csv"
  }

  response = requests.post(
      "https://api.dcycle.io/api/v1/purchases/bulk/csv",
      headers=headers,
      json=payload
  )

  result = response.json()
  upload_url = result["upload_url"]
  file_id = result["file_id"]

  print(f"Upload URL obtained: {upload_url[:50]}...")
  print(f"File ID: {file_id}")
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const axios = require('axios');

  const apiKey = process.env.DCYCLE_API_KEY;
  const orgId = process.env.DCYCLE_ORG_ID;
  const userId = process.env.DCYCLE_USER_ID;

  const headers = {
    'Authorization': `Bearer ${apiKey}`,
    'x-organization-id': orgId,
    'x-user-id': userId,
    'Content-Type': 'application/json'
  };

  const payload = {
    file_name: '2024_q1_purchases.csv'
  };

  axios.post(
    'https://api.dcycle.io/api/v1/purchases/bulk/csv',
    payload,
    { headers }
  )
  .then(response => {
    const { upload_url, file_id } = response.data;
    console.log('Upload URL:', upload_url.substring(0, 50) + '...');
    console.log('File ID:', file_id);
  })
  .catch(error => console.error(error));
  ```
</CodeGroup>

### Successful Response

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "upload_url": "https://dcycle-purchases.s3.amazonaws.com/files/dcycle/...",
  "file_name": "2024_q1_purchases",
  "file_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "destination_file_key": "files/dcycle/.../2024_q1_purchases.csv",
  "message": "Upload pre-signed url generated"
}
```

## Step 2: Upload CSV to S3

Use the presigned URL to upload your CSV file directly to S3:

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  # The presigned URL already includes authentication parameters
  curl -X PUT "${UPLOAD_URL}" \
    -H "Content-Type: text/csv" \
    --data-binary @2024_q1_purchases.csv
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests

  # Read CSV file
  with open('2024_q1_purchases.csv', 'rb') as file:
      csv_data = file.read()

  # Upload to S3
  response = requests.put(
      upload_url,
      data=csv_data,
      headers={'Content-Type': 'text/csv'}
  )

  if response.status_code == 200:
      print("✅ File uploaded successfully")
  else:
      print(f"❌ Error: {response.status_code}")
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const fs = require('fs');
  const axios = require('axios');

  // Read CSV file
  const csvData = fs.readFileSync('2024_q1_purchases.csv');

  // Upload to S3
  axios.put(uploadUrl, csvData, {
    headers: { 'Content-Type': 'text/csv' }
  })
  .then(() => console.log('✅ File uploaded successfully'))
  .catch(error => console.error('❌ Error:', error));
  ```
</CodeGroup>

<Warning>
  The presigned URL expires in **15 minutes**. Make sure to upload the file within that time.
</Warning>

## CSV Format

The CSV must follow this structure for creating purchases:

```csv theme={"theme":{"light":"github-light","dark":"github-dark"}}
quantity,unit_id,sector,product_name,purchase_date,country,description,recycled,frequency,supplier_business_name,supplier_country
25000.50,eur-unit-uuid,IT Services,Cloud Computing,2024-01-15,ES,AWS monthly costs,0.0,monthly,AWS EMEA SARL,IE
15000.00,eur-unit-uuid,Office Supplies,Printer Paper,2024-01-10,ES,Q1 paper purchase,0.3,quarterly,Office Depot,ES
8500.75,eur-unit-uuid,Transportation,Courier Services,2024-01-20,ES,Monthly DHL costs,0.0,monthly,DHL Express,ES
```

### Required Columns

| Column | Description | Valid Values | Example |
| - | - | - | - |
| `quantity` | Purchase amount | Positive number | `25000.50` |
| `unit_id` | UUID of currency unit | UUID from `/api/v1/units?type=fiat_currency` | `eur-unit-uuid` |
| `sector` | Economic sector | Sector name from EXIOBASE | `"IT Services"` |
| `product_name` | Product or service name | Free text | `"Cloud Computing"` |
| `purchase_date` | Date of purchase | YYYY-MM-DD format | `2024-01-15` |
| `country` | *Deprecated.* Country of purchase — no longer used for emission calculations (the country is taken from the supplier). Kept for backward compatibility. | ISO 3166-1 alpha-2 code | `ES` |

### Optional Columns

| Column | Description | Example |
| - | - | - |
| `description` | Purchase description or notes | `"Q1 AWS infrastructure"` |
| `recycled` | Recycled content percentage (0.0 to 1.0) | `0.3` (30% recycled) |
| `frequency` | Purchase frequency | `once`, `weekly`, `monthly`, `quarterly`, `yearly` |
| `supplier_business_name` | Supplier company name | `"AWS EMEA SARL"` |
| `supplier_country` | Supplier country | `ES`, `US`, etc. |

### Column Details

**quantity**:

* Must be positive number
* Typically in currency units (EUR, USD, etc.)
* No negative values allowed

**unit\_id**:

* Must be a valid UUID from the Units endpoint
* For spend-based purchases, use currency units (EUR, USD, GBP, etc.)
* Get valid unit IDs: `GET /api/v1/units?type=fiat_currency`

**sector**:

* Economic sector classification
* Based on EXIOBASE or NAICS codes
* Examples: `"IT Services"`, `"Manufacturing"`, `"Transportation"`, `"Construction"`

**product\_name**:

* Descriptive name of the product or service
* Free text field
* Examples: `"Cloud Computing"`, `"Office Furniture"`, `"Legal Services"`

**purchase\_date**:

* Must be today or in the past
* Format: `YYYY-MM-DD`
* Used to select appropriate emission factors by year

**country**:

* ISO 3166-1 alpha-2 country code (2 letters)
* Where the purchase was made
* Common values: `ES` (Spain), `US` (USA), `FR` (France), `DE` (Germany), `GB` (UK)

**frequency**:

* For recurring purchases
* Values: `once`, `weekly`, `monthly`, `quarterly`, `yearly`
* Default: `once`

**recycled**:

* Percentage of recycled content
* Range: `0.0` (0%) to `1.0` (100%)
* Reduces emission calculations
* Leave empty or `0.0` if not applicable

### Download Template

<Card title="Download CSV Template" icon="download" href="/templates/purchases_bulk_upload_template.csv">
  Template with correct format and sample data
</Card>

## Step 3: Asynchronous Processing

Once the CSV is uploaded:

1. **Validation**: The system validates format, units, sectors, and dates
2. **Processing**: Each row is processed and purchases are created
3. **Emission Calculation**: CO2e values calculated using economic input-output factors
4. **Supplier Matching**: Suppliers are matched or created based on business\_name and country
5. **Notification**: (Coming soon) You'll receive a webhook when complete

<Info>
  Processing can take from a few seconds to several minutes depending on file size and number of rows.
</Info>

## Step 4: Verify Results

After processing, you can query your purchases:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# List purchases
curl "https://api.dcycle.io/api/v1/purchases" \
  -H "Authorization: Bearer ${DCYCLE_API_KEY}" \
  -H "x-organization-id: ${DCYCLE_ORG_ID}" \
  -H "x-user-id: ${DCYCLE_USER_ID}"
```

## Complete Example Script

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests
  import os
  from time import sleep

  class DcyclePurchases:
      def __init__(self, api_key, org_id, user_id):
          self.api_key = api_key
          self.org_id = org_id
          self.user_id = user_id
          self.base_url = "https://api.dcycle.io"
          self.headers = {
              "Authorization": f"Bearer {api_key}",
              "x-organization-id": org_id,
              "x-user-id": user_id,
              "Content-Type": "application/json"
          }

      def upload_csv(self, csv_file_path):
          """Complete CSV upload: presigned URL + upload + verification"""

          # 1. Get presigned URL
          file_name = os.path.basename(csv_file_path)
          response = requests.post(
              f"{self.base_url}/api/v1/purchases/bulk/csv",
              headers=self.headers,
              json={"file_name": file_name}
          )
          response.raise_for_status()

          upload_url = response.json()["upload_url"]
          file_id = response.json()["file_id"]

          print(f"✅ Presigned URL obtained (File ID: {file_id})")

          # 2. Upload file
          with open(csv_file_path, 'rb') as f:
              upload_response = requests.put(
                  upload_url,
                  data=f,
                  headers={'Content-Type': 'text/csv'}
              )
          upload_response.raise_for_status()

          print(f"✅ File uploaded successfully")

          # 3. Wait for processing (optional)
          print("⏳ Processing... (this may take a few minutes)")
          sleep(30)  # Wait 30 seconds

          # 4. Verify purchases
          purchases = self.get_purchases()
          print(f"✅ Total purchases: {purchases['total']}")

          return file_id

      def get_purchases(self, page=1, size=50):
          """List purchases"""
          response = requests.get(
              f"{self.base_url}/api/v1/purchases",
              headers=self.headers,
              params={"page": page, "size": size}
          )
          response.raise_for_status()
          return response.json()

  # Usage
  if __name__ == "__main__":
      client = DcyclePurchases(
          api_key=os.getenv("DCYCLE_API_KEY"),
          org_id=os.getenv("DCYCLE_ORG_ID"),
          user_id=os.getenv("DCYCLE_USER_ID")
      )

      client.upload_csv("2024_q1_purchases.csv")
  ```

  ```bash Bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
  #!/bin/bash

  # Variables
  API_KEY="${DCYCLE_API_KEY}"
  ORG_ID="${DCYCLE_ORG_ID}"
  USER_ID="${DCYCLE_USER_ID}"
  CSV_FILE="2024_q1_purchases.csv"
  BASE_URL="https://api.dcycle.io"

  # 1. Get presigned URL
  echo "📤 Requesting presigned URL..."
  RESPONSE=$(curl -s -X POST "${BASE_URL}/api/v1/purchases/bulk/csv" \
    -H "Authorization: Bearer ${API_KEY}" \
    -H "x-organization-id: ${ORG_ID}" \
    -H "x-user-id: ${USER_ID}" \
    -H "Content-Type: application/json" \
    -d "{\"file_name\": \"${CSV_FILE}\"}")

  UPLOAD_URL=$(echo $RESPONSE | jq -r '.upload_url')
  FILE_ID=$(echo $RESPONSE | jq -r '.file_id')

  echo "✅ URL obtained (File ID: ${FILE_ID})"

  # 2. Upload file
  echo "📤 Uploading CSV file..."
  curl -X PUT "${UPLOAD_URL}" \
    -H "Content-Type: text/csv" \
    --data-binary @"${CSV_FILE}"

  echo "✅ File uploaded successfully"
  echo "⏳ File is being processed..."
  ```
</CodeGroup>

## Common Errors

### 400 Bad Request - Missing file\_name

**Cause:** Field required

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "detail": "Field required",
  "field": "file_name"
}
```

**Solution:** Include the `file_name` parameter in the request body.

### 403 Forbidden - URL Expired

**Cause:** The presigned URL expired (15 minutes)

**Solution:** Request a new presigned URL and upload the file immediately.

### 422 Validation Error - CSV Format

**Cause:** The CSV has incorrect format or missing required columns

**Solution:** Verify that your CSV has all required columns and correct format.

### 422 Validation Error - Invalid Unit ID

**Cause:** `unit_id` doesn't exist or is not a currency unit

**Solution:** Use the Units endpoint to get valid currency unit IDs, or use EUR/USD/GBP units.

### 422 Validation Error - Invalid Date

**Cause:** `purchase_date` is in the future

**Solution:** Ensure all purchase dates are today or in the past.

### 422 Validation Error - Invalid Country

**Cause:** Country code is not ISO 3166-1 alpha-2 format

**Solution:** Use valid 2-letter country codes (ES, US, FR, DE, GB, etc.).

## Limits and Recommendations

| Limit | Value |
| - | - |
| **Maximum file size** | 100 MB |
| **Maximum rows per CSV** | 50,000 |
| **URL expiration time** | 15 minutes |
| **Processing time** | \~1 second per 100 purchases |

<Tip>
  For very large purchase lists (>50k records), consider splitting them into multiple smaller CSVs.
</Tip>

## Best Practices

### Data Preparation

1. **Clean supplier names**: Use consistent naming (e.g., "AWS EMEA SARL" not "Amazon Web Services" or "AWS")
2. **Validate unit IDs**: Check that unit\_id exists before upload using `/api/v1/units?type=fiat_currency`
3. **Standardize sectors**: Use consistent sector names across purchases
4. **Include recycled content**: For products with recycled materials, include the percentage

### Error Handling

After upload, check for purchases with `status: "error"`:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def check_for_errors():
    """Check if any purchases failed to process"""
    response = requests.get(
        "https://api.dcycle.io/api/v1/purchases",
        headers=headers,
        params={"filter_by": "status:eqerror"}
    )

    purchases = response.json()

    if purchases['total'] > 0:
        print(f"⚠️ {purchases['total']} purchases have errors:")
        for purchase in purchases['items']:
            print(f"  - {purchase.get('product_name', 'Unnamed')}: {purchase.get('sector')}")

    return purchases['total']

# Check after processing
errors_count = check_for_errors()
if errors_count == 0:
    print("✅ All purchases processed successfully")
```

### Incremental Updates

To update existing purchases or add new ones:

1. Export current purchases
2. Modify or add rows
3. Re-upload (system will handle duplicates)

## Economic Sectors

Common sectors for spend-based emission calculations:

### Manufacturing & Production

* `Manufacturing` - General manufacturing
* `Food & Beverage Manufacturing`
* `Textile Manufacturing`
* `Chemical Manufacturing`

### Services

* `IT Services` - Technology and software services
* `Professional Services` - Consulting, legal, accounting
* `Financial Services` - Banking, insurance
* `Healthcare Services` - Medical services

### Transportation & Logistics

* `Transportation` - General transport services
* `Air Transport` - Air freight and passenger
* `Water Transport` - Sea and inland water transport
* `Land Transport` - Road and rail transport

### Construction & Real Estate

* `Construction` - Building and infrastructure
* `Real Estate` - Property services

### Other

* `Office Supplies` - Stationery, equipment
* `Utilities` - Water, waste management
* `Agriculture` - Agricultural products

<Info>
  Emission factors vary significantly by sector. Using the correct sector classification is critical for accurate carbon accounting.
</Info>

## Special Notes

### Spend-Based Method

Purchases uploaded via CSV use the **spend-based** method:

* Emissions calculated based on economic input-output tables
* Formula: `CO2e = Amount (EUR) × Emission Factor (kg CO2e/EUR)`
* Emission factors vary by sector, product, and country/region
* Factors updated annually based on latest data

### Recycled Content

When `recycled` percentage is provided:

* Emission factor is reduced proportionally
* Example: 30% recycled content → 30% lower emissions
* Based on lifecycle assessment data
* Applies to materials like paper, plastics, metals

### Supplier Matching

The system automatically:

* Creates new suppliers if not found
* Matches by business\_name and country
* Stores supplier data for future purchases
* Links purchases to supplier records

### Country Codes

Use ISO 3166-1 alpha-2 country codes:

* Spain: `ES`
* United States: `US`
* France: `FR`
* Germany: `DE`
* United Kingdom: `GB`
* Italy: `IT`
* Netherlands: `NL`

## Related Endpoints

<CardGroup cols={2}>
  <Card title="List Purchases" icon="list" href="/api-docs/purchases/list">
    View uploaded purchases
  </Card>

  <Card title="List Units" icon="ruler" href="/api-reference/legacy/units/list">
    Get currency units for purchases
  </Card>

  <Card title="List Facilities" icon="building" href="/api-docs/facilities/list">
    View your facilities
  </Card>

  <Card title="Authentication" icon="key" href="/api-docs/authentication">
    Learn about API authentication
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.