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

# Bulk CSV upload

> Pipe a purchase order or shipment manifest through /v1/calc/bulk. Format, error handling, and tier requirements.

## When to use bulk

Any workload that would otherwise loop `/v1/calc` should switch to
`/v1/calc/bulk`. One bulk call:

* Counts as **one** rate-limit request (60 rpm) and one quota entry.
* Pro-rates freight and insurance across rows by line value.
* Returns per-row results and per-row errors without failing the batch.
* Caps at 500 rows per call.

Bulk requires Growth tier or higher. Free and Pro cannot mint the
key needed to call it. See [Authentication](/authentication).

## Request shape

Ship-level fields (destination, currency, incoterm, transport mode,
freight, insurance) live at the top. Line-level fields go in `rows[]`.

```json theme={null}
{
  "destination_country": "US",
  "currency": "USD",
  "incoterm": "FOB",
  "transport_mode": "ocean",
  "freight": 1200,
  "insurance": 180,
  "rows": [
    {
      "description": "Bluetooth earbuds",
      "hs_code": "8517.62",
      "quantity": 500,
      "unit_value": 12.50,
      "origin_country": "CN"
    },
    {
      "description": "USB-C charging cable",
      "hs_code": "8544.42",
      "quantity": 1000,
      "unit_value": 1.20,
      "origin_country": "CN"
    }
  ]
}
```

## CSV to JSON mapping

If you are starting from a CSV, the column-to-field mapping is:

| CSV column       | JSON field       | Required               |
| ---------------- | ---------------- | ---------------------- |
| `description`    | `description`    | Yes                    |
| `hs_code`        | `hs_code`        | Yes                    |
| `quantity`       | `quantity`       | Yes                    |
| `unit_value`     | `unit_value`     | Yes                    |
| `origin_country` | `origin_country` | Yes (ISO 3166 alpha-2) |
| `weight_kg`      | `weight_kg`      | No                     |

Ship-level fields (`destination_country`, `currency`, `incoterm`,
`transport_mode`, `freight`, `insurance`) must come from a header
row, a form field, or a filename convention. They are not per-line.

## Convert a CSV and post it

```python theme={null}
import csv, json, os, requests

with open("po.csv") as f:
    reader = csv.DictReader(f)
    rows = [
        {
            "description": r["description"],
            "hs_code": r["hs_code"],
            "quantity": float(r["quantity"]),
            "unit_value": float(r["unit_value"]),
            "origin_country": r["origin_country"],
        }
        for r in reader
    ]

payload = {
    "destination_country": "US",
    "currency": "USD",
    "incoterm": "FOB",
    "transport_mode": "ocean",
    "freight": 1200,
    "insurance": 180,
    "rows": rows[:500],  # enforce the cap client-side
}

r = requests.post(
    "https://www.landedfees.com/api/v1/calc/bulk",
    headers={
        "Authorization": f"Bearer {os.environ['LANDEDFEES_API_KEY']}",
        "Content-Type": "application/json",
    },
    json=payload,
    timeout=60,
)
r.raise_for_status()
print(json.dumps(r.json()["aggregate"], indent=2))
```

## Response shape

```json theme={null}
{
  "id": "5f6b7c8d-1234-4abc-9def-0123456789ab",
  "results": [
    { "line_index": 0, "ok": true, "landed_cost": 8123.45, ... },
    { "line_index": 1, "ok": true, "landed_cost":  1450.10, ... }
  ],
  "errors": [
    { "line_index": 12, "reason": "invalid HS code", "field": "hs_code" }
  ],
  "aggregate": {
    "total_rows": 250,
    "ok_rows": 249,
    "error_rows": 1,
    "total_value": 128400.00,
    "total_duty":   38520.00,
    "total_taxes":     0.00,
    "total_fees":    412.10,
    "total_landed": 167332.10,
    "currency": "USD"
  }
}
```

## Handling row errors

Row errors are non-fatal. A batch of 500 with 3 invalid rows returns
`200` with `ok_rows: 497` and 3 entries in `errors[]`. Re-post the
failed rows after fixing them; use `line_index` to correlate back to
your source CSV.

If you need atomic behavior ("either all 500 succeed or none apply"),
implement it client-side: post the batch, and if `error_rows > 0`,
do not commit the results to your downstream system.

## Freight and insurance pro-rating

Freight and insurance ship at the shipment level, not per row. The
engine pro-rates each across rows by line value:

```
row.freight   = shipment.freight   * (row.line_value / sum(row.line_value))
row.insurance = shipment.insurance * (row.line_value / sum(row.line_value))
```

The pro-rated values are folded into each row's customs value. If
you have per-row freight (unusual), post one shipment per row.
