REST API

Pull carrier records, documents, and packets on demand. Bearer-token authenticated, paginated, JSON.

Base URL

https://carrierpacket.link/api/v1

Authentication

Every request needs an API key in the Authorization header. Generate keys at Embedded → API keys. We hash and store keys (never the plaintext) and show you the full value once on creation — copy it then.

curl https://carrierpacket.link/api/v1/submissions \
  -H "Authorization: Bearer cpl_a1b2c3_x9z7y…"

For quick tests you can also pass the key as a query parameter: ?api_key=cpl_…. Don't use this in production — it leaks into server logs and browser history.

IP allowlist

When you generate a key, you can optionally restrict it to specific source IPs (comma-separated, literal addresses; CIDR support is coming). Requests from any other IP get a 403 ip_not_allowed.

Tracking last use

Every successful authenticated request bumps last_used_at + last_used_ip on the key. The Embedded → API keys table surfaces both so you can audit usage and spot keys that haven't been touched in a while.

Response shape

Success

Single objects:

{ "data": { "id": 1138, "legal_name": "EVERGREEN SHIPPERS LLC", ... } }

Lists include a pagination envelope:

{
  "data": [ { ... }, { ... } ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 234,
    "has_more": true
  }
}

List endpoints accept ?page=N (1-indexed) and ?limit=N (default 50, max 200). Pagination is offset-based.

Errors

{ "error": { "code": "unauthorized", "message": "Invalid or revoked API key." } }
HTTPcodeMeaning
400bad_requestMissing or malformed query parameter.
401unauthorizedMissing or invalid API key.
403ip_not_allowedSource IP isn't on the key's allowlist.
404not_foundResource doesn't exist or doesn't belong to your account.
500server_errorUnexpected. Email support with the request details.

Submissions

A submission is one carrier signing one packet. Tenant-scoped — you only see submissions for packets you own.

List submissions

GET /v1/submissions

Query params: page, limit, status (1=Pending verify, 2=Verified, 3=Rejected, 4=Archived).

curl 'https://carrierpacket.link/api/v1/submissions?status=2&limit=10' \
  -H "Authorization: Bearer cpl_…"

Get a submission

GET /v1/submissions/{id}
curl https://carrierpacket.link/api/v1/submissions/1138 \
  -H "Authorization: Bearer cpl_…"

Sample response (truncated):

{
  "data": {
    "id": 1138,
    "packet_id": 7,
    "hashid": "8b3e4f...",
    "status": 2,
    "mc_number": "896325",
    "usdot_number": "2569360",
    "legal_name": "EVERGREEN SHIPPERS LLC",
    "phone": "+1 (509) 991-8269",
    "physical_address": "13323 N MAYFAIR LN",
    "physical_city": "SPOKANE",
    "physical_state": "WA",
    "physical_zip": "99208",
    "dispatcher_email": "dispatch@carrier.com",
    "services_provided": ["Flatbed", "Reefer"],
    "auth_fullname": "John W. Smith",
    "auth_email": "john@carrier.com",
    "signed_at": "2026-04-26 14:33:18",
    "email_verified_at": "2026-04-26 14:51:02"
  }
}

Submission fields

FieldNotes
id integerSequential. Use this in URLs (e.g. /v1/submissions/{id}) and in your TMS as the foreign key.
packet_id integerWhich packet template the carrier signed.
hashid string (64-char hex)Globally unique unguessable id. Use as your idempotency key.
status int (1–4)1 Pending verify · 2 Verified · 3 Rejected · 4 Archived. See Concepts → Submission.
mc_number, usdot_number stringFMCSA identifiers. Either may be empty depending on what the carrier has.
legal_name, dba_name stringCarrier's registered business name and "doing business as," from FMCSA.
phone stringPre-formatted (e.g. +1 (509) 991-8269).
physical_address, physical_city, physical_state, physical_zip stringCarrier's physical location.
mailing_address, mailing_city, mailing_state, mailing_zip stringMailing address; often same as physical.
dispatcher_name, dispatcher_email, dispatcher_phone, dispatcher_fax, dispatcher_url stringThe dispatch contact the carrier provided. dispatcher_email is who you'd email about loads.
services_provided array of stringe.g. ["Flatbed", "Reefer"]. Decoded from JSON for you.
services_notes stringFree-form note about equipment / capacity.
new_entrant_status, operating_status stringFMCSA snapshot at sign time (e.g. "AUTHORIZED FOR Property").
agreement_accepted int (0/1)1 if the carrier checked the "I have read the agreement" box.
auth_fullname, auth_email stringPerson who actually e-signed. auth_email is what we send the verification link to.
auth_taxid stringFederal Tax ID / EIN (formatted "88 - 7893200").
contract_datetime datetimeWhen the form initially rendered (sent in a hidden field; useful for audit).
signed_at datetimeWhen the carrier hit "Sign here." The canonical "submission timestamp."
email_verified_at datetime · nullableWhen the carrier clicked the verification link. Null until they do.
agent_email string · nullableIf the link they used had ?agent=<email>, the value lands here. Useful for attribution.
ip_address stringCarrier's IP at sign time. Audit field.
created, updated datetimeStandard timestamps.

List documents for a submission

GET /v1/submissions/{id}/documents

Returns every uploaded document tied to one submission. Each row carries a derived days_until_expiry field (null if no expires_on is set).

{
  "data": [
    {
      "id": 482,
      "doc_type": "coi",
      "original_filename": "evergreen-coi-2026.pdf",
      "mime_type": "application/pdf",
      "size_bytes": 412573,
      "expires_on": "2026-09-15",
      "days_until_expiry": 142,
      "created": "2026-04-26 14:33:21"
    }
  ]
}

Documents

Cross-submission view of every document the broker has on file. Designed for renewal-tracking dashboards.

List documents

GET /v1/documents

Query params:

ParamTypeWhat it does
doc_type stringfilterOne of w9, coi, authority, references, ach, other.
submission_id intfilterScope to one submission.
expiring_within int (days)filterOnly docs whose expires_on is within N days. Excludes docs with no expiration date. Use this for renewal reports.
page, limitpaginationStandard pagination.

Example: documents expiring in the next 30 days

curl 'https://carrierpacket.link/api/v1/documents?expiring_within=30&doc_type=coi' \
  -H "Authorization: Bearer cpl_…"

You get back a sorted list (soonest expiry first) with days_until_expiry on each row. Negative values mean the document is already expired.

Hit this every morning from a cron and you've got a free "carriers with insurance expiring this month" report. Pair with the submission_id field to email each carrier from your TMS.

Get a document

GET /v1/documents/{id}
File streaming is coming soon. The current endpoint returns metadata only (filename, MIME, size, expiry). To download the actual file bytes, log into Carriers and use the document card on the carrier's profile.

Document fields

FieldNotes
id integerDocument primary key.
submission_id integerForeign key to /v1/submissions/{submission_id}.
doc_type stringOne of w9, coi, authority, references, ach, other.
original_filename stringCarrier's original filename. Useful for display; we rename to a UUID on disk.
mime_type stringDetected via finfo_file() (not the browser-supplied header).
size_bytes integerFile size in bytes. Capped at 10 MB per file at upload time.
expires_on date · nullableDocument expiration if set (e.g. COI valid-through date). Null if not provided.
days_until_expiry int · nullableDerived. Negative if past, null if no expires_on. Computed in the broker's timezone, DST-safe.
created datetimeWhen the file was uploaded.

Packets

List packets

GET /v1/packets

Query params: page, limit, status (0=Draft, 1=Active, 2=Archived).

curl 'https://carrierpacket.link/api/v1/packets?status=1' \
  -H "Authorization: Bearer cpl_…"

Get a packet

GET /v1/packets/{id}

Returns the packet metadata plus the full settings JSON (branding choices, doc-collection toggles, default lookup, etc.) and the embed_token you'd put in an iframe.

{
  "data": {
    "id": 7,
    "name": "Standard Carrier Packet",
    "slug": "standard-packet",
    "status": 1,
    "embed_token": "abc123def456...",
    "settings": {
      "fg": "#40b9c7",
      "bg": "#f7fafc",
      "filesEnabled": true,
      "docs": { "w9": true, "coi": true, "authority": true, "references": false, "ach": false, "other": true }
    },
    "signed_count": 47,
    "last_activity": "2026-04-26 14:33:18"
  }
}

Packet fields

FieldNotes
id integerPacket primary key.
name stringInternal label only — carriers don't see this.
slug stringURL-safe identifier per broker. Unique per (user_id, slug).
status int0 Draft (hidden) · 1 Active (carriers can sign) · 2 Archived.
embed_token stringThe unguessable token in https://carrierpacket.link/c/<token>. Drop into an iframe.
settings objectDesigner state — branding, layout, doc requirements, agreement text. Free-form JSON; see the Designer for the canonical shape.
signed_count integerLifetime count of submissions on this packet.
last_activity datetime · nullableWhen the most recent carrier signed. Null if none yet.
created, updated datetimeStandard timestamps.

Error handling

Every error returns the same envelope:

{ "error": { "code": "ip_not_allowed", "message": "Your IP (76.135.204.132) isn't on this key's allowlist." } }

The code field is the stable contract — integrate against that, not the human message (which we'll polish over time).

HTTP / codeWhat happenedWhat to do
400 bad_requestMissing or malformed query parameter (e.g. ?status=99).Fix the request; re-read the param spec for the endpoint.
401 unauthorizedMissing API key, malformed Authorization header, or revoked key.Confirm you're sending Authorization: Bearer cpl_… with the actual plaintext (not the prefix). If the key was rotated, get a new one from Embedded.
403 ip_not_allowedSource IP isn't on this key's allowlist.Add the IP to the key's allowlist on Embedded, or remove the allowlist entirely.
404 not_foundResource doesn't exist or isn't yours.The 404 is intentional even when the ID exists for a different broker — we don't leak existence across tenants. Double-check you're using the right account's key.
500 server_errorUnexpected. Probably a bug on our end.Email support with the request URL + a timestamp. We log every 500 with full context.

Retry strategy

For 5xx and network failures, retry with exponential backoff: 1s, 2s, 4s, 8s. Cap at 5 retries. For 4xx, don't retry — the request itself is wrong, retrying won't help.

Idempotency: every endpoint here is read-only, so retrying is safe by construction. When write endpoints ship, we'll publish an idempotency-key header.

What's coming

Write endpointsPOST/PATCH/DELETE for packets & submission status. Read-only for v1.
Document file streamingGET /v1/documents/{id}/file — bytes-as-response with proper Content-Disposition.
CIDR in IP allowlistsToday: literal IPs only. Coming: 10.0.0.0/8-style ranges.
Per-key rate limitsCurrently uncapped. Will be a per-minute budget visible on each key's row.
Sandbox keysSeparate cpl_test_… prefix so you can build against a non-production data set.
OpenAPI specFor SDK generation and Postman.