Concepts & data model

The vocabulary you'll see throughout the docs and the API. Glossary first, then the relationships between resources.

Glossary

Broker

You. The freight broker (or 3PL, or dispatch company) onboarding carriers. In the data model, you're a users row. Everything you create — packets, signups, API keys, webhooks — is scoped to your broker account.

Carrier

The trucking company you're onboarding. Identified by MC number and/or US DOT number, both of which we look up against FMCSA in real time during the form.

Carriers don't have logins on CarrierPacket.Link — they fill out a packet, sign, and verify their email. Their identity in the system is captured as a submission.

Packet

A reusable template you build in the Designer. Defines: branding (font, colors, dark/light), which fields the carrier fills out, which documents they upload, the broker–carrier agreement text.

You can have multiple packets per broker account — common pattern is one for each commodity type (van, reefer, flatbed, power-only). Exactly one is starred as your primary at any time; that's the packet served at /p/<your-slug> and embedded by the Embedded snippet. Re-star a different packet to swap which one is live — no need to update any URLs.

A per-attribution share URL at /a/<link-slug>. Hand each agent (or trade-show booth, or marketing channel) their own link. Each link routes one extra notification destination — an email and/or SMS — that fires on top of your broker-level recipients whenever a carrier signs through it.

Manage them on the Links page. Slugs are tiny (2–3 chars), immutable, and resolve to the broker's primary packet — so re-starring a different packet automatically updates what every link serves.

Submission

One carrier completing one packet. The row that lands in your Carriers list. Carries all the data the carrier provided (legal name, MC, USDOT, addresses, dispatch contact, services, signature, IP, timestamps) plus a status:

statusmeaning
1Pending verification (carrier signed, but hasn't clicked the verification email link yet)
2Verified (email confirmed)
3Rejected (you marked it bad from the Carriers detail modal)
4Archived (you set it aside)

Document

A file the carrier uploaded with their submission — W-9, COI (proof of insurance), MC authority letter, references, ACH info, etc. Each row stores the original filename, MIME type, size, and (importantly) expires_on.

The expires_on field powers the renewal-tracking workflow — GET /api/v1/documents?expiring_within=30 returns every doc due to expire in the next 30 days.

API key

A bearer token your code sends in the Authorization header to authenticate against the public REST API. Format: cpl_<6-char-prefix>_<32-char-body>. Generate at Embedded → API keys.

We hash and store keys (never the plaintext) and show you the full value once on creation. Each key has an optional IP allowlist and tracks last_used_at.

Webhook

An HTTPS endpoint we POST event payloads to. You configure them at Embedded → Webhooks with a URL, a list of events to subscribe to (carrier.signed, carrier.verified, carrier.rejected), and we hand you a signing secret. Up to 5 webhooks per broker.

Every payload is signed with HMAC-SHA256 in the X-CPL-Signature header so you can verify it really came from us.

How they relate

users (broker)
  ├── slug                              short slug for /p/<slug> (immutable, derived from id)
  ├── primary_packet_id                 → packets.id; the starred packet served by /p/<slug>
  ├── packets                           one broker, many packets
  │     └── carrier_submissions         one packet, many submissions
  │           ├── link_id               → links.id; set when carrier hit /a/<link-slug>
  │           └── carrier_documents     one submission, many docs (≤6)
  ├── links                             per-attribution share URLs (/a/<slug>), 1 notify-email + 1 notify-sms each
  ├── api_keys                          ≤ unbounded; revoke = soft-delete
  ├── webhooks                          ≤ 5 per broker
  └── notify_emails / notify_smses      ≤ 3 each, in JSON columns on the user row

Tenant isolation is enforced everywhere: every query filters by user_id = <current broker>, both in the dashboard and the API. There's no cross-broker data access path, even by ID guessing — GET /api/v1/submissions/42 returns 404 if submission 42 isn't yours.

URL conventions

PatternWhat it is
/p/<user_slug>Carrier-facing form for the broker's primary (starred) packet. The default link you share or embed.
/a/<link_slug>Same packet, but the submission is tagged with that link — routes notifications to the link's email/SMS in addition to broker-level ones.
/c/<embed_token>Legacy per-packet share link. Still resolved for backwards compatibility, but no longer generated by the UI.
/api/v1/<resource>REST API endpoints. Bearer-token auth required.
/app/*Authenticated broker dashboard.
/app/docs/*These docs. Public, no auth.

IDs vs tokens

You'll see two kinds of identifiers on submissions:

  • id — integer primary key, sequential. Use this in the dashboard URL (/app/carriers.php?id=1138) and as the {id} in REST endpoints.
  • hashid — 64-char unguessable hex string. Used internally; reserved for future "share this signed packet" links. Don't depend on the format.

Brokers have a users.slug (deterministic 2–3-char base62 derived from users.id). It's what carriers see in /p/<slug>. Immutable — the URL you put on a business card stays valid forever, and starring a different packet is what swaps what it serves.

Links have their own links.slug, same flavor (deterministic, immutable). One per agent / channel / campaign. Listed and managed on the Links page.