# DYLI 3PL API — complete integration reference

Production base URL: `https://www.dyli.io/api/3pl/{company}`  
Purpose: read company-scoped inventory, validate destinations, quote shipping and fulfillment fees, create idempotent shipments, read tracking, and move eligible inventory into a DYLI user's vault.

DYLI provides the `{company}` slug during onboarding. DYLI's warehouse receives, checks, stores, packs, and fulfills the inventory. Your application selects inventory and shipping options and submits server-side API requests.

## Authentication

Use either a standard DYLI Public API key or a company-specific 3PL key:

```http
x-api-key: your_key
```

Standard Public API keys also support:

```http
Authorization: Bearer dyli_live_...
```

- Create standard keys at `https://www.dyli.io/requestapi`.
- Keep keys in a backend environment variable or secret manager.
- Never put API keys or payment transaction hashes in public frontend code.
- Use a separate key per integration and rotate exposed credentials.
- Standard keys return `X-RateLimit-*` headers and may return `Retry-After`.

`GET /inventory` has an additional account-level authorization check. It only accepts a standard Public API key whose owning DYLI account can already access the requested company in the 3PL Inventory Dashboard. A company-specific partner key does not bypass this check. A valid key tied to another account or company receives `403`.

## Identifiers and limits

| Inventory kind | Request location | Identifier |
| --- | --- | --- |
| Graded card/slab | `certs[]` | `cert` certificate number; `certNumber` is accepted by some routes. |
| Sealed product | `products[]` | DYLI `productId`; aliases include `product_id`, `dyliProductId`, `linkedProductId`. |

- Keep graded and sealed rows in their respective arrays. A shipment may contain both arrays.
- Shipping supports up to 100 total items.
- Vault-to-DYLI supports up to 10 total units.
- Monetary values are USD unless a field says USDC.
- Dates use ISO 8601.
- Countries use two-letter ISO codes such as `US` and `CA`.
- Inventory must be in the company's live 3PL inventory for production shipping/vaulting.

## Standard physical-shipping flow

1. Validate the destination with `GET /address`.
2. Request options with `POST /rates` using the confirmed address and live inventory.
3. Present a shipping option and save its `courier_id` and `total_amount`.
4. Send exactly `total_amount` in native USDC to the correct DYLI address for the chosen chain.
5. Submit `POST /order` with the same address/inventory, selected `courierId`, payment `txHash`, and `paymentChain`.
6. Save both `shipment_id` and `tracking_key`.
7. Poll `GET /status` no faster than every 30 seconds while pending.

`/rates` does not reserve inventory. The order checks availability again.

## Endpoint map

| Method and path | Purpose |
| --- | --- |
| `GET /inventory` | List company inventory with current on-hand and stock status. |
| `GET /address` | Validate and normalize a physical destination. |
| `GET /username` | Validate a DYLI username and active wallet. |
| `POST /username` | Same username validation with a JSON body. |
| `POST /rates` | Quote physical shipping or a sealed-product vault transfer. |
| `POST /order` | Create an idempotent physical shipment. |
| `GET /status` | Read pending shipment or carrier tracking state. |
| `GET /vault` | Read vault request requirements, limits, options, and fee. |
| `POST /vault` | Move sealed inventory to a DYLI username or create/vault an enabled new product. |

## Inventory

Use a standard Public API key tied to an account with access to the company:

```bash
curl --get "https://www.dyli.io/api/3pl/$DYLI_3PL_COMPANY/inventory" \
  -H "x-api-key: $DYLI_API_KEY" \
  --data-urlencode "location=live" \
  --data-urlencode "pageSize=100"
```

Each `data` row includes `inventory_id`, `inventory_type` (`graded` or `sealed`), `name`, `image_url`, `brand`, `subcategory`, `cert`, `product_id`, `received_quantity`, `on_hand_quantity`, `location`, and `status`. Use `cert` for graded shipping requests and `product_id` for sealed shipping requests; `inventory_id` is only the warehouse row identifier.

Query parameters:

- `location`: `live` (default), `staging`, or `all`.
- `status`: `all` (default), `in_stock`, `low_stock`, or `out_of_stock`.
- `lowStockThreshold`: positive whole number, default `1`; alias `low_stock_threshold`.
- `productId`: exact sealed DYLI product id; alias `product_id`.
- `cert`: exact grading certificate number.
- `page`: 1-based, default `1`.
- `pageSize`: default `100`, maximum `250`; aliases `page_size` and `limit`.

At the selected threshold, `out_of_stock` means on hand is `0`, `low_stock` means `1` through the threshold, and `in_stock` means above the threshold. Staging rows cannot be shipped or vaulted.

## Address validation

```bash
curl --get "https://www.dyli.io/api/3pl/$DYLI_3PL_COMPANY/address" \
  -H "x-api-key: $DYLI_3PL_API_KEY" \
  --data-urlencode "name=Jamie Doe" \
  --data-urlencode "email=jamie@example.com" \
  --data-urlencode "phone=+1 212 555 0100" \
  --data-urlencode "address_line_1=123 Main Street" \
  --data-urlencode "address_line_2=Suite 4" \
  --data-urlencode "city_locality=New York" \
  --data-urlencode "state_province=NY" \
  --data-urlencode "postal_code=10001" \
  --data-urlencode "country_code=US"
```

Required: `name`, valid `email`, `phone`, `address_line_1`, `city_locality`, `state_province`, `postal_code`, and `country_code`. `address_line_2` is optional. Russia is not supported. Recipient names and address fields must use printable English/ASCII characters; `/address`, `/rates`, and `/order` all enforce this before contacting a carrier.

A valid address returns `200` with `valid: true` and normalized `address`. A correctable address returns `200` with `valid: false`, `corrected: true`, and `recommended_address`. Let the customer confirm the recommendation, then use the same accepted destination in `/rates` and `/order`.

Unsupported characters return `400` with `valid: false`, code `NON_ASCII_SHIPPING_ADDRESS`, a user-facing `error`, and the affected `fields`. Treat this as a customer-input response, not a carrier or server failure.

## Username validation

```bash
curl --get "https://www.dyli.io/api/3pl/$DYLI_3PL_COMPANY/username" \
  -H "x-api-key: $DYLI_3PL_API_KEY" \
  --data-urlencode "username=alex"
```

`@alex` and `alex` are equivalent. POST accepts:

```json
{ "username": "alex" }
```

Aliases are `dyliUsername` and `dyli_username`.

Valid response:

```json
{
  "valid": true,
  "username": "alex",
  "profileUrl": "https://www.dyli.io/profile/alex",
  "wallet": "0x1234...ABCD"
}
```

Unknown users return `200` with `valid: false` and `reason`; this is a validation result, not an API failure.

## Shipping and vault rates

### Graded shipment

```bash
curl "https://www.dyli.io/api/3pl/$DYLI_3PL_COMPANY/rates" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "x-api-key: $DYLI_3PL_API_KEY" \
  -d '{
    "address": {
      "name": "Jamie Doe",
      "email": "jamie@example.com",
      "phone": "+1 212 555 0100",
      "address_line_1": "123 Main Street",
      "address_line_2": "Suite 4",
      "city": "New York",
      "state": "NY",
      "postal_code": "10001",
      "country_alpha2": "US"
    },
    "certs": [
      { "cert": "12345678", "value": 250 },
      { "cert": "87654321", "value": 175 }
    ],
    "includeDdp": false,
    "insurance": true
  }'
```

### Sealed shipment

```json
{
  "address": {
    "name": "Jamie Doe",
    "email": "jamie@example.com",
    "phone": "+1 212 555 0100",
    "address_line_1": "123 Main Street",
    "city": "New York",
    "state": "NY",
    "postal_code": "10001",
    "country_alpha2": "US"
  },
  "products": [
    { "productId": 29022, "qty": 2, "value": 95 }
  ],
  "includeDdp": false,
  "insurance": true
}
```

Request fields:

- `address`: required for physical shipping; the rates/order shape uses `city`, `state`, and `country_alpha2`.
- `certs`: graded rows with certificate and optional declared USD `value`.
- `products`: sealed rows with DYLI product ID, positive whole-number `qty`, and optional declared USD `value`.
- `includeDdp`: when true, include destination duties/taxes (DDP); default false means the recipient may pay on delivery (DDU).
- `insurance`: price insurance from submitted values; default false.
- `vaultWithDyli`: quote a sealed-only vault transfer; no physical address is required.
- `test`: lab/test shipping behavior described below.

Normal rate response:

```json
{
  "shippingOptions": [
    {
      "id": "option_0",
      "carrier": "UPS Ground",
      "estimated_days": 4,
      "courier_id": "courier-service-id",
      "shipping_fees": "12.40",
      "redemption_fees": "7.97",
      "total_amount": "20.37"
    }
  ],
  "summary": {
    "slabCount": 2,
    "sealedCount": 0,
    "gradedCount": 2,
    "totalValue": "425.00",
    "parcelCount": 1,
    "box": "7 x 5 x 2",
    "destination": "New York, NY, US"
  }
}
```

Save `courier_id`, not response-local `id: option_0`. Up to five options are returned cheapest first. Media Mail and DHL are excluded; FedEx is excluded for Dubai destinations.

Vault quote:

```json
{
  "products": [{ "productId": 29022, "qty": 2 }],
  "vaultWithDyli": true
}
```

```json
{
  "shippingOptions": [],
  "vaultWithDyli": {
    "required": true,
    "unit_fee": "3.00",
    "total_fee": "6.00",
    "amount_usdc": 6,
    "txHashRequired": true
  }
}
```

Vault quote is sealed-only. Request current rates immediately before payment; do not hardcode fee totals.

## USDC payment

The 3PL API does not initiate a payment. Your backend sends native USDC, then submits the transaction hash.

EVM recipient:

```text
0x04eaa73b518783010a5a5dc471eea89c4c34ef44
```

Solana recipient:

```text
F4PbwT93NVHYJ2uwsbNRarRWczNS9bgeP3warW2wUjzZ
```

Supported `paymentChain` values:

`abstract`, `avalanche`, `arbitrum`, `base`, `ethereum`, `optimism`, `polygon`, `hyperevm`, `monad`, `solana`.

- Match the recipient to the chain; never send Solana assets to the EVM address or vice versa.
- Use native USDC, not a bridged or similarly named token.
- Solana signatures are case-sensitive.
- Physical shipping amount is the selected rate's exact `total_amount`.
- Vault amount is `vaultWithDyli.amount_usdc`.
- The transaction hash is also an idempotency/tracking key; never reuse it for another shipment or company.
- DYLI currently reviews 3PL payment transactions manually.
- Card payment is disabled unless DYLI separately approves another company billing arrangement.

## Create a physical shipment

Send the same address and inventory used for the rate, plus the chosen `courierId` and payment evidence:

```bash
curl "https://www.dyli.io/api/3pl/$DYLI_3PL_COMPANY/order" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "x-api-key: $DYLI_3PL_API_KEY" \
  -d '{
    "address": {
      "name": "Jamie Doe",
      "email": "jamie@example.com",
      "phone": "+1 212 555 0100",
      "address_line_1": "123 Main Street",
      "address_line_2": "Suite 4",
      "city": "New York",
      "state": "NY",
      "postal_code": "10001",
      "country_alpha2": "US"
    },
    "certs": [{ "cert": "12345678", "value": 250 }],
    "courierId": "courier-service-id",
    "txHash": "0x...",
    "paymentChain": "base",
    "includeDdp": false,
    "insurance": true
  }'
```

Optional `tax_id` aliases: `taxId`, `consignee_tax_id`.

Created response includes `success`, carrier `shipment_id`, label/tracking URLs, courier, `tracking_key`, test/inventory-adjustment state, and a packing/value/destination summary. Save `shipment_id` and `tracking_key`.

`202` means the request is accepted but carrier creation is pending:

```json
{
  "success": true,
  "pending": true,
  "shipment_id": "0x...",
  "tracking_key": "0x...",
  "retryable": true,
  "retryAfterSeconds": 30
}
```

Do not create a second order. Poll `/status` or safely retry the identical request with the same `txHash`. DYLI returns the existing shipment if already created. Inventory is reduced only after shipment creation; if `inventoryAdjusted` is false, inspect `inventoryError` and contact DYLI before changing inventory yourself.

## Shipment status

```bash
curl --get "https://www.dyli.io/api/3pl/$DYLI_3PL_COMPANY/status" \
  -H "x-api-key: $DYLI_3PL_API_KEY" \
  --data-urlencode "shipment_id=0x..."
```

`shipment_id` accepts the returned `tracking_key`, original `txHash`, or carrier/Easyship shipment ID. Aliases are `txHash` and `tx_hash`.

Pending state includes `status`, attempt count, retryability, next/last attempt timestamps, insurance, and incoterms. Completed carrier state includes tracking number, external tracking, tracking-page URL, ETA, checkpoints, and pagination metadata.

Poll pending shipments no faster than once every 30 seconds.

## Vault with DYLI

Call `GET /vault` to read current supported fields, fee, and limits.

### Transfer existing sealed products to a username

Validate the username, quote with `/rates` and `vaultWithDyli: true`, send the USDC fee, then:

```bash
curl "https://www.dyli.io/api/3pl/$DYLI_3PL_COMPANY/vault" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "x-api-key: $DYLI_3PL_API_KEY" \
  -d '{
    "username": "alex",
    "products": [
      { "productId": 29022, "qty": 2 },
      { "productId": 2942, "qty": 1 }
    ],
    "usdcTxHash": "0x...",
    "paymentChain": "base"
  }'
```

Maximum: 10 total units. The response includes mode, username/profile, product/token/supply/link data, mint and transfer transaction hashes, recipient wallet, fee reconciliation, inventory adjustment, and notification state.

### Create and vault a new product

This path requires DYLI to enable product creation for the company. DYLI matches inventory by row ID, cert, or name and reuses an already-linked product when possible.

```json
{
  "requesterWalletAddress": "0x...",
  "usdcTxHash": "0x...",
  "paymentChain": "base",
  "quantity": 1,
  "externalRequestId": "order-1042",
  "name": "2024 Pokemon Example PSA 10",
  "image": "https://example.com/card-front.png",
  "price": 99,
  "subcategory": "Graded Card",
  "cert": "12345678"
}
```

Required: `requesterWalletAddress`, `usdcTxHash`, `name`, `price`, `subcategory`, and `image` or `images`. Quantity defaults to 1, maximum 10.

Allowed subcategories: `Graded Card`, `Graded Booster Pack`, `Raw Card`, `Booster Pack`, `Booster Box`, `Sealed Product`, `Collectible`.

Vault operations mint/transfer blockchain assets. Do not automatically retry an unknown `500`; the chain operation may already have started. Inspect returned hashes and contact DYLI first.

## Testing

The 3PL API uses the provisioned production company/key with `test: true`; it does not use a separate sandbox hostname.

1. Validate a real deliverable destination.
2. Call `/rates` with an item identity and `test: true`.
3. Select the returned `courier_id`.
4. Call `/order` with the same shipment, a unique non-empty test `txHash`, and `test: true`.
5. Check `/status` normally.

Test transaction hashes do not need to be onchain. Use a new value for each logical shipment and the same value to retry that shipment.

Test mode skips live-inventory availability/decrement and real sale recording, but it **does create a carrier shipment/label**. Use a deliverable address, clearly identify the test recipient, and coordinate cancellation with DYLI. Vault operations do not support test mode. Never send `test: true` for a customer shipment.

## Errors and retry behavior

Normal error shape:

```json
{ "error": "Missing required courierId" }
```

Some carrier failures include `details` and `retryable`.

| Status | Meaning and action |
| --- | --- |
| `200` | Success; validation routes may use `valid: false`. |
| `202` | Accepted/pending. Poll status; do not create a new logical order. |
| `400` | Invalid data, unavailable inventory, or unsupported option. Fix input. |
| `401` | Missing/incorrect API key. |
| `403` | Standard key invalid, disabled, revoked, or expired. |
| `404` | Company, shipment, or record not found. |
| `405` | Method not allowed. |
| `409` | Transaction hash already assigned elsewhere. Use a new payment for a new shipment. |
| `422` | Inventory cannot be packed with current configuration. Change shipment or contact DYLI. |
| `429` | Rate limited. Wait for `Retry-After`. |
| `500` | DYLI failure. Shipping retries may be safe with the same transaction; vault retries require review. |
| `502` | Carrier/upstream failure. Respect `retryable`; otherwise requote/contact DYLI. |

## Production checklist

1. Confirm company slug and server-side API key.
2. Use live inventory identities from the DYLI 3PL dashboard.
3. Validate and let the customer confirm the address.
4. Preserve the exact rated address/inventory and selected `courier_id` through order creation.
5. Pay the exact returned amount on the declared chain.
6. Persist `txHash`, `shipment_id`, and `tracking_key`.
7. Retry shipment creation only with the same logical request and same `txHash`.
8. Poll pending status at 30-second-or-longer intervals.
9. Escalate inventory-adjustment discrepancies instead of mutating warehouse records independently.
10. Never blindly retry a vault request after an ambiguous server failure.

Human-readable docs: `https://www.dyli.io/docs/3pl`  
Read API reference: `https://www.dyli.io/docs/llms/read-api.md`  
Commerce API reference: `https://www.dyli.io/docs/llms/commerce-api.md`
