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." } }
| HTTP | code | Meaning |
|---|---|---|
| 400 | bad_request | Missing or malformed query parameter. |
| 401 | unauthorized | Missing or invalid API key. |
| 403 | ip_not_allowed | Source IP isn't on the key's allowlist. |
| 404 | not_found | Resource doesn't exist or doesn't belong to your account. |
| 500 | server_error | Unexpected. 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
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
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
| Field | Notes |
|---|---|
| id integer | Sequential. Use this in URLs (e.g. /v1/submissions/{id}) and in your TMS as the foreign key. |
| packet_id integer | Which 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 string | FMCSA identifiers. Either may be empty depending on what the carrier has. |
| legal_name, dba_name string | Carrier's registered business name and "doing business as," from FMCSA. |
| phone string | Pre-formatted (e.g. +1 (509) 991-8269). |
| physical_address, physical_city, physical_state, physical_zip string | Carrier's physical location. |
| mailing_address, mailing_city, mailing_state, mailing_zip string | Mailing address; often same as physical. |
| dispatcher_name, dispatcher_email, dispatcher_phone, dispatcher_fax, dispatcher_url string | The dispatch contact the carrier provided. dispatcher_email is who you'd email about loads. |
| services_provided array of string | e.g. ["Flatbed", "Reefer"]. Decoded from JSON for you. |
| services_notes string | Free-form note about equipment / capacity. |
| new_entrant_status, operating_status string | FMCSA 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 string | Person who actually e-signed. auth_email is what we send the verification link to. |
| auth_taxid string | Federal Tax ID / EIN (formatted "88 - 7893200"). |
| contract_datetime datetime | When the form initially rendered (sent in a hidden field; useful for audit). |
| signed_at datetime | When the carrier hit "Sign here." The canonical "submission timestamp." |
| email_verified_at datetime · nullable | When the carrier clicked the verification link. Null until they do. |
| agent_email string · nullable | If the link they used had ?agent=<email>, the value lands here. Useful for attribution. |
| ip_address string | Carrier's IP at sign time. Audit field. |
| created, updated datetime | Standard timestamps. |
List documents for a submission
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
Query params:
| Param | Type | What it does |
|---|---|---|
| doc_type string | filter | One of w9, coi, authority, references, ach, other. |
| submission_id int | filter | Scope to one submission. |
| expiring_within int (days) | filter | Only docs whose expires_on is within N days. Excludes docs with no expiration date. Use this for renewal reports. |
| page, limit | pagination | Standard 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.
submission_id field to email each carrier from your TMS.Get a document
Document fields
| Field | Notes |
|---|---|
| id integer | Document primary key. |
| submission_id integer | Foreign key to /v1/submissions/{submission_id}. |
| doc_type string | One of w9, coi, authority, references, ach, other. |
| original_filename string | Carrier's original filename. Useful for display; we rename to a UUID on disk. |
| mime_type string | Detected via finfo_file() (not the browser-supplied header). |
| size_bytes integer | File size in bytes. Capped at 10 MB per file at upload time. |
| expires_on date · nullable | Document expiration if set (e.g. COI valid-through date). Null if not provided. |
| days_until_expiry int · nullable | Derived. Negative if past, null if no expires_on. Computed in the broker's timezone, DST-safe. |
| created datetime | When the file was uploaded. |
Packets
List 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
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
| Field | Notes |
|---|---|
| id integer | Packet primary key. |
| name string | Internal label only — carriers don't see this. |
| slug string | URL-safe identifier per broker. Unique per (user_id, slug). |
| status int | 0 Draft (hidden) · 1 Active (carriers can sign) · 2 Archived. |
| embed_token string | The unguessable token in https://carrierpacket.link/c/<token>. Drop into an iframe. |
| settings object | Designer state — branding, layout, doc requirements, agreement text. Free-form JSON; see the Designer for the canonical shape. |
| signed_count integer | Lifetime count of submissions on this packet. |
| last_activity datetime · nullable | When the most recent carrier signed. Null if none yet. |
| created, updated datetime | Standard 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 / code | What happened | What to do |
|---|---|---|
| 400 bad_request | Missing or malformed query parameter (e.g. ?status=99). | Fix the request; re-read the param spec for the endpoint. |
| 401 unauthorized | Missing 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_allowed | Source 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_found | Resource 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_error | Unexpected. 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 endpoints | POST/PATCH/DELETE for packets & submission status. Read-only for v1. |
| Document file streaming | GET /v1/documents/{id}/file — bytes-as-response with proper Content-Disposition. |
| CIDR in IP allowlists | Today: literal IPs only. Coming: 10.0.0.0/8-style ranges. |
| Per-key rate limits | Currently uncapped. Will be a per-minute budget visible on each key's row. |
| Sandbox keys | Separate cpl_test_… prefix so you can build against a non-production data set. |
| OpenAPI spec | For SDK generation and Postman. |