Parselv1.0.0

Sortation

Record packages moving through Parsel locations — create containers, sort packages into them, group them into loads, and depart them.

The Network API records how packages move through Parsel locations: which container each package is sorted into and which containers leave together. To sort a package is to scan it and put it in the container for where it goes next. Every sort also adds a tracking update to the package.

Overview

A location is a physical node within the Parsel network where packages are received, processed, consolidated, or dispatched. Containers are created with a location and carrier. When a truck is ready to leave, a load is created to group the containers leaving the location together. When the load leaves, you depart it.

All endpoints authenticate with Authorization: Bearer <your_token>. See Authentication, and Errors for what each status code means.

The flow

Step 1: Pick a location

Retrieve all Parsel locations available to the authenticated user. Use the returned id as the location_id parameter in subsequent requests.

Endpoint: GET https://api.parsel.app/api/locations

curl https://api.parsel.app/api/locations \
  -H "Authorization: Bearer <your_token>"
const res = await fetch("https://api.parsel.app/api/locations", {
  headers: { Authorization: `Bearer ${process.env.PARSEL_TOKEN}` },
});
if (!res.ok) throw new Error(`Parsel API ${res.status}`);
const { data: locations } = await res.json();
const locationId = locations[0].id;
import os
import requests

HEADERS = {"Authorization": f"Bearer {os.environ['PARSEL_TOKEN']}"}

resp = requests.get("https://api.parsel.app/api/locations", headers=HEADERS)
resp.raise_for_status()
locations = resp.json()["data"]
location_id = locations[0]["id"]
Response
{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Parsel Chicago"
      // ... address, coordinates and timezone
    }
  ]
}

Step 2: Create a container

Create a container at the location for one carrier. Every package you sort into the container must be going to that carrier.

The location must hand packages to the carrier you send. If it doesn't, the request returns 422. See Create a container for the carriers you can send.

Endpoint: POST https://api.parsel.app/api/containers

curl https://api.parsel.app/api/containers \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "location_id": "550e8400-e29b-41d4-a716-446655440000",
    "carrier": "USPS"
  }'
const res = await fetch("https://api.parsel.app/api/containers", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PARSEL_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ location_id: locationId, carrier: "USPS" }),
});
if (!res.ok) throw new Error(`Parsel API ${res.status}`);
const { data: container } = await res.json();
resp = requests.post(
    "https://api.parsel.app/api/containers",
    headers=HEADERS,
    json={"location_id": location_id, "carrier": "USPS"},
)
resp.raise_for_status()
container = resp.json()["data"]
Response
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440001",
    "status": "ACTIVE",
    "carrier": "USPS",
    "network_location_id": "550e8400-e29b-41d4-a716-446655440000",
    "created_at": "2026-10-30T12:00:00Z"
  }
}

Step 3: Sort packages into the container

Call this once for every package scanned into the container. Send the barcode exactly as read from the package. If you know the package's tracking_code, send it and it's used instead of the barcode. If you know the package's carrier, send it too. It's optional, but it helps Parsel identify the package when a barcode could belong to more than one carrier. If you have the package's parcel, send it too.

Endpoint: POST https://api.parsel.app/api/containers/{id}/sort

curl https://api.parsel.app/api/containers/550e8400-e29b-41d4-a716-446655440001/sort \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "barcode": "420303139400100000000000123456",
    "carrier": "USPS",
    "parcel": {
      "height": 1.1,
      "length": 1.1,
      "width": 1.1,
      "length_unit": "IN",
      "weight": 1.1,
      "weight_unit": "OZ"
    }
  }'
const res = await fetch(
  `https://api.parsel.app/api/containers/${container.id}/sort`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PARSEL_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      barcode: "420303139400100000000000123456",
      carrier: "USPS",
      parcel: {
        height: 1.1,
        length: 1.1,
        width: 1.1,
        length_unit: "IN",
        weight: 1.1,
        weight_unit: "OZ",
      },
    }),
  }
);
if (!res.ok) throw new Error(`Parsel API ${res.status}`);
const { data: sortEvent } = await res.json();
resp = requests.post(
    f"https://api.parsel.app/api/containers/{container['id']}/sort",
    headers=HEADERS,
    json={
        "barcode": "420303139400100000000000123456",
        "carrier": "USPS",
        "parcel": {
            "height": 1.1,
            "length": 1.1,
            "width": 1.1,
            "length_unit": "IN",
            "weight": 1.1,
            "weight_unit": "OZ",
        },
    },
)
resp.raise_for_status()
sort_event = resp.json()["data"]

Each sort records a SORT scan event and returns it. tracking_code is the package's tracking code, which can differ from the barcode you scanned. Here, the USPS barcode starts with 42030313, the destination ZIP code, which isn't part of the tracking code.

Response
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440002",
    "event_type": "SORT",
    "tracking_code": "9400100000000000123456",
    "shipping_label_id": "550e8400-e29b-41d4-a716-446655440003",
    "network_container_id": "550e8400-e29b-41d4-a716-446655440001",
    "network_location_id": "550e8400-e29b-41d4-a716-446655440000",
    "created_at": "2026-10-30T12:05:00Z"
  }
}

A package that doesn't belong in this container returns 422, for example one going to a different carrier, or one that isn't routed through this location. Sorting a package that's already in a container at this location for the same carrier returns 409.

Step 4: Create a load

A load is a set of containers leaving the same location together. The containers must not be empty or already in a load. Once a container is in a load, it no longer accepts packages.

Endpoint: POST https://api.parsel.app/api/loads

curl https://api.parsel.app/api/loads \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "container_ids": [
      "550e8400-e29b-41d4-a716-446655440001",
      "550e8400-e29b-41d4-a716-446655440004"
    ]
  }'
const res = await fetch("https://api.parsel.app/api/loads", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PARSEL_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ container_ids: containerIds }),
});
if (!res.ok) throw new Error(`Parsel API ${res.status}`);
const { data: load } = await res.json();
resp = requests.post(
    "https://api.parsel.app/api/loads",
    headers=HEADERS,
    json={"container_ids": container_ids},
)
resp.raise_for_status()
load = resp.json()["data"]
Response
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440005"
  }
}

Step 5: Depart the load

When the load leaves your location, depart it.

Optionally, send the trip's provider and its bill of lading as bol_id to link the containers to the trip carrying them. Send both or neither. The load departs even if no trip matches.

Endpoint: POST https://api.parsel.app/api/loads/{id}/depart

curl https://api.parsel.app/api/loads/550e8400-e29b-41d4-a716-446655440005/depart \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "WARP",
    "bol_id": "S-123456-2610"
  }'
const res = await fetch(
  `https://api.parsel.app/api/loads/${load.id}/depart`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PARSEL_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ provider: "WARP", bol_id: "S-123456-2610" }),
  }
);
if (!res.ok) throw new Error(`Parsel API ${res.status}`);
const { data: departEvents } = await res.json();
resp = requests.post(
    f"https://api.parsel.app/api/loads/{load['id']}/depart",
    headers=HEADERS,
    json={"provider": "WARP", "bol_id": "S-123456-2610"},
)
resp.raise_for_status()
depart_events = resp.json()["data"]

The response lists one DEPART scan event per container in the load. tracking_code is null because only sort events refer to a package.

Response
{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440006",
      "event_type": "DEPART",
      "tracking_code": null,
      "network_container_id": "550e8400-e29b-41d4-a716-446655440001",
      "network_location_id": "550e8400-e29b-41d4-a716-446655440000",
      "created_at": "2026-10-30T18:00:00Z"
    },
    {
      "id": "550e8400-e29b-41d4-a716-446655440007",
      "event_type": "DEPART",
      "tracking_code": null,
      "network_container_id": "550e8400-e29b-41d4-a716-446655440004",
      "network_location_id": "550e8400-e29b-41d4-a716-446655440000",
      "created_at": "2026-10-30T18:00:00Z"
    }
  ]
}

A load can only depart once. After it departs, the API treats it as not found, so departing it again returns 404.

Appendix: Change a load before it departs

Until a load departs, you can add containers to it or remove them, for example when a container is scanned late or put on the wrong truck. Once it departs, both return 404, so these examples use another load, 550e8400-e29b-41d4-a716-446655440009, that hasn't departed yet.

Add containers

The containers you add follow the same rules as creating a load: at the load's location, not empty and not already in a load. A container that's already in this load is left as it is.

Endpoint: POST https://api.parsel.app/api/loads/{id}/containers

curl https://api.parsel.app/api/loads/550e8400-e29b-41d4-a716-446655440009/containers \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "container_ids": ["550e8400-e29b-41d4-a716-446655440008"]
  }'
const res = await fetch(
  `https://api.parsel.app/api/loads/${loadId}/containers`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PARSEL_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ container_ids: [lateContainerId] }),
  }
);
if (!res.ok) throw new Error(`Parsel API ${res.status}`);
resp = requests.post(
    f"https://api.parsel.app/api/loads/{load_id}/containers",
    headers=HEADERS,
    json={"container_ids": [late_container_id]},
)
resp.raise_for_status()
Response
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440009"
  }
}

Remove a container

The container leaves the load and accepts packages again. You can remove every container from a load, but a load with no containers can't depart.

Endpoint: DELETE https://api.parsel.app/api/loads/{id}/containers/{container_id}

curl -X DELETE https://api.parsel.app/api/loads/550e8400-e29b-41d4-a716-446655440009/containers/550e8400-e29b-41d4-a716-446655440010 \
  -H "Authorization: Bearer <your_token>"
const res = await fetch(
  `https://api.parsel.app/api/loads/${loadId}/containers/${containerId}`,
  {
    method: "DELETE",
    headers: { Authorization: `Bearer ${process.env.PARSEL_TOKEN}` },
  }
);
if (!res.ok) throw new Error(`Parsel API ${res.status}`);
resp = requests.delete(
    f"https://api.parsel.app/api/loads/{load_id}/containers/{container_id}",
    headers=HEADERS,
)
resp.raise_for_status()

A successful removal returns 204 with no body.

On this page