> ## 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

# Bulk Upload Purchases

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>
  This method is **recommended** for bulk uploads. Purchases 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`       | Country of purchase     | 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-docs/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>
