You need to look up a motor carrier by DOT number from PHP and ship an integration that returns reliable identification data without guessing about live rates or wait times. By the end of this guide you’ll query the FreightPulse Carrier Lookup API from PHP, parse the envelope, and safely handle auth, quotas, and caching in production.
What you’ll implement
This tutorial walks through a PHP Carrier Lookup integration that searches carriers by DOT or text query. You’ll test the endpoint with curl, then write a compact PHP client that:
- Sends the X-API-Key header or runs as a guest within daily limits.
- Parses the canonical envelope {"success":true,"data":{...}}.
- Extracts fields you actually use in TMS/visibility flows: legal_name, status, safety_rating, drivers, and address.
- Implements basic timeouts, error handling, and response caching to stay under quotas and reduce latency.
Reference links for later: Register and Documentation.
API basics you must know
- Auth: header X-API-Key: YOUR_API_KEY. Guest without key: 100 requests/day per IP.
- Envelope: {"success":true,"data":{...}}. Always read from data; do not assume top-level fields contain results.
- Port congestion is IMF PortWatch, lagged ~6-7 days, 7-day avg vs 90-day baseline — not live AIS. If you surface congestion alongside carrier info, label it clearly as lagged.
- Do not quote a freight-rates USD fare as today’s price. If you show rates at all, label them historical or quote-source specific, not live.
Carrier Lookup endpoint used below:
GET https://freightpulsehq.com/api/v1/carriers?q=54283
Quick test with curl
Verify your network path and response envelope first. The following is copy-pasteable and uses the documented header:
curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/carriers?q=54283"
Official fixture (copy of a real response shape you should expect; values shown are exactly as provided):
{"success":true,"data":{"type":"trucking","query":"54283","results":[{"dot_number":54283,"legal_name":"SWIFT TRANSPORTATION COMPANY OF ARIZONA LLC","status":"active","safety_rating":"S","drivers":12883,"address":{"city":"Phoenix","state":"AZ","country":"US"}}]}}
Field notes you will actually use:
- type: category of entity returned ("trucking" here).
- query: the search term you sent (string).
- results: array of carriers.
- dot_number: integer DOT identifier to use as your primary key for joins.
- legal_name: the carrier’s registered name for audit and label printing.
- status: operational status string you can surface in onboarding checks.
- safety_rating: short code; keep mapping logic in your app, don’t transform at ingestion.
- drivers: integer count useful for scale heuristics (treat as snapshot, not a guarantee).
- address: nested location details (city, state, country) for region filters.
PHP: minimal client with curl
The snippet below calls the same endpoint, sets the X-API-Key header, validates HTTP status, parses the JSON envelope, and extracts the first carrier. It keeps timeouts strict to avoid hanging workers and includes simple file-based caching to reduce duplicate calls.
PHP example (single-file, curl)
<?php
declare(strict_types=1);
function fp_get_carrier(string $query, ?string $apiKey = null, int $ttlSeconds = 600): array {
$endpoint = 'https://freightpulsehq.com/api/v1/carriers?q=' . rawurlencode($query);
$cacheKey = 'fp_carriers_' . sha1($endpoint . '|' . ($apiKey ?? 'guest'));
$cacheFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $cacheKey . '.json';
// Simple cache to ease guest quota: 100 requests/day per IP
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $ttlSeconds)) {
$cached = file_get_contents($cacheFile);
if ($cached !== false) {
$decoded = json_decode($cached, true);
if (is_array($decoded)) return $decoded;
}
}
$ch = curl_init($endpoint);
if ($ch === false) {
throw new RuntimeException('Failed to init curl');
}
$headers = [];
if ($apiKey) {
$headers[] = 'X-API-Key: ' . $apiKey; // Auth header
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 5, // seconds
CURLOPT_TIMEOUT => 10, // hard cap
CURLOPT_HTTPHEADER => $headers,
CURLOPT_USERAGENT => 'fp-php-client/1.0',
]);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$errstr = curl_error($ch);
curl_close($ch);
if ($errno !== 0) {
throw new RuntimeException('Network error: ' . $errstr, $errno);
}
if ($status < 200 || $status >= 300) {
throw new RuntimeException('HTTP ' . $status . ' from FreightPulse');
}
$json = json_decode((string)$body, true);
if (!is_array($json)) {
throw new RuntimeException('Invalid JSON');
}
if (!array_key_exists('success', $json) || $json['success'] !== true) {
throw new RuntimeException('API indicated failure');
}
if (!isset($json['data']) || !is_array($json['data'])) {
throw new RuntimeException('Missing data envelope');
}
// Persist to cache on success
@file_put_contents($cacheFile, json_encode($json));
return $json;
}
// Example usage:
$apiKey = getenv('FP_API_KEY') ?: null; // or hardcode YOUR_API_KEY for local testing
$response = fp_get_carrier('54283', $apiKey);
// Pull the first carrier result safely
$results = $response['data']['results'] ?? [];
$first = $results[0] ?? null;
if ($first) {
$dot = $first['dot_number'] ?? null;
$name = $first['legal_name'] ?? '';
$status = $first['status'] ?? '';
$rating = $first['safety_rating'] ?? '';
$drivers = $first['drivers'] ?? null;
$city = $first['address']['city'] ?? '';
$state = $first['address']['state'] ?? '';
$country = $first['address']['country'] ?? '';
echo 'DOT: ' . $dot . PHP_EOL;
echo 'Name: ' . $name . PHP_EOL;
echo 'Status: ' . $status . ' | Safety rating: ' . $rating . PHP_EOL;
echo 'Drivers: ' . $drivers . PHP_EOL;
echo 'Address: ' . $city . ', ' . $state . ', ' . $country . PHP_EOL;
} else {
echo "No results for query.\n";
}
Notes:
- Environment variable FP_API_KEY is optional; without it you operate as a guest with 100 requests/day per IP.
- The example caches whole envelopes for 10 minutes by default. Adjust TTL based on how often you query the same DOTs.
- Keep timeouts strict in workers to avoid tying up PHP-FPM pools.
JavaScript example (Node.js fetch)
If you need to test cross-language parity or wire up a small CLI, this shows the exact same call with the same header and envelope parsing. Values are illustrative except for field names and the endpoint.
import fetch from 'node-fetch';
async function lookupCarrier(query, apiKey) {
const url = 'https://freightpulsehq.com/api/v1/carriers?q=' + encodeURIComponent(query);
const headers = {};
if (apiKey) headers['X-API-Key'] = apiKey;
const res = await fetch(url, { headers, redirect: 'follow' });
if (!res.ok) {
throw new Error('HTTP ' + res.status + ' from FreightPulse');
}
const json = await res.json();
if (!json.success || !json.data) {
throw new Error('API indicated failure or missing data');
}
const first = (json.data.results || [])[0];
if (!first) return null;
return {
dot_number: first.dot_number,
legal_name: first.legal_name,
status: first.status,
safety_rating: first.safety_rating,
drivers: first.drivers,
address: first.address
};
}
lookupCarrier('54283', process.env.FP_API_KEY)
.then(c => console.log(c))
.catch(err => console.error(err));
Interpreting and storing results
The carrier record is compact and stable enough to store directly in your database for deduplication and quick subsequent reads. Suggested approach:
- Primary key: dot_number (integer).
- Immutable fields to store as-is: legal_name.
- Semi-static fields to refresh periodically: status, safety_rating, drivers, address.
- Envelope persistence: keep the raw JSON in an audit column for traceability.
When joining into a TMS or onboarding workflow:
- Use legal_name for display and document generation.
- Gate onboarding or tendering based on status and safety_rating thresholds you define.
- Treat drivers as an indicative count, not a service-level commitment.
Error handling, quotas, and caching
Guest usage is capped at 100 requests/day per IP. Implement durable caching and backoffs from day one:
- Cache: File or Redis cache keyed by query string, with a TTL between 10 minutes and 24 hours depending on your data freshness needs.
- Retries: Retry idempotent GETs on transient 5xx with jittered backoff. Do not retry on 4xx except maybe 429 after a delay.
- Timeouts: Set both connect and request timeouts to protect workers. The PHP example uses 5s connect and 10s total.
- Concurrency: Pool outbound requests in your job queue rather than from web requests when possible.
- Monitoring: Log JSON decode errors and non-2xx statuses with the query string to speed up debugging.
To run above guest limits, send your key with X-API-Key: YOUR_API_KEY and consolidate queries via caching to avoid waste.
Production considerations specific to FreightPulse
- Envelope-first parsing: Always read success and data before touching results. This keeps your parser resilient to additional top-level fields.
- Timestamps and timezone: If you persist retrieved_at, store server time in UTC alongside the raw envelope for auditability.
- Idempotency: GET by query is naturally idempotent; deduplicate by normalized query and timestamp window in your cache.
- Pagination: If you expand beyond exact DOT queries to broader text searches, design for multiple results. Check the array length before assuming a single match. For API-wide pagination or cursor semantics, refer to the Documentation.
- Data freshness: Carrier identity fields are relatively stable. If you also display port congestion in the same UI, label it as lagged ~6–7 days (IMF PortWatch 7-day avg vs 90-day baseline), not live AIS. Avoid mixing those concepts in SLAs.
- No live rate quotes: Do not present any USD fare as today’s price. If you store or show rates from other sources, fence them off with clear labels.
End-to-end PHP script you can drop in
This variant wraps everything into a single callable and prints a normalized row you can copy into a CSV or log. It uses headers and fields exactly as documented.
<?php
declare(strict_types=1);
function carrier_lookup(string $q, ?string $apiKey = null): array {
$url = 'https://freightpulsehq.com/api/v1/carriers?q=' . rawurlencode($q);
$h = [];
if ($apiKey) $h[] = 'X-API-Key: ' . $apiKey;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_HTTPHEADER => $h,
]);
$body = curl_exec($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($body === false) throw new RuntimeException('Network error: ' . $err);
if ($status < 200 || $status >= 300) throw new RuntimeException('HTTP ' . $status);
$json = json_decode($body, true);
if (!is_array($json) || ($json['success'] ?? null) !== true) {
throw new RuntimeException('Unexpected envelope');
}
$results = $json['data']['results'] ?? [];
if (!$results) return [];
$out = [];
foreach ($results as $r) {
$out[] = [
'dot_number' => $r['dot_number'] ?? null,
'legal_name' => $r['legal_name'] ?? '',
'status' => $r['status'] ?? '',
'safety_rating'=> $r['safety_rating'] ?? '',
'drivers' => $r['drivers'] ?? null,
'city' => $r['address']['city'] ?? '',
'state' => $r['address']['state'] ?? '',
'country' => $r['address']['country'] ?? '',
];
}
return $out;
}
// Run:
$apiKey = getenv('FP_API_KEY') ?: null;
$rows = carrier_lookup('54283', $apiKey);
foreach ($rows as $r) {
echo implode(',', [
$r['dot_number'],
$r['legal_name'],
$r['status'],
$r['safety_rating'],
$r['drivers'],
$r['city'],
$r['state'],
$r['country'],
]) . PHP_EOL;
}
Testing checklist
- With and without X-API-Key header to validate guest fallback and header wiring.
- Malformed queries (empty string) return empty results handling without exceptions.
- Network timeouts trigger a fast failure and do not block worker pools.
- Cache hits reduce outbound calls; verify TTL behavior by inspecting temp files or Redis keys.
- JSON schema drift tolerance: your code reads via keys and guards against missing optional fields.
FAQ
Do I need an API key to start?
Not strictly. You can call the endpoint as a guest, limited to 100 requests/day per IP. Add X-API-Key: YOUR_API_KEY to raise limits for production.
How should I handle multiple results for a text query?
Treat results as an array and apply your own matching logic (e.g., exact DOT match first, then strict legal_name comparison). Do not assume a single element.
Is port congestion live?
No. FreightPulse port congestion uses IMF PortWatch and is lagged about 6–7 days (7-day average vs 90-day baseline). It is not live AIS wait time.
Can I cache responses safely?
Yes. For DOT lookups, cache for minutes to hours depending on your update policy. Always store the raw envelope and a retrieved_at timestamp in UTC for audits.
Are there rate or price quotes I can display?
Do not quote a freight-rates USD fare as today’s price. If you display rates from elsewhere, label them clearly and keep them separate from identity data.
Ready to move beyond the guest quota and wire this into your TMS? Create an account and get your key: Register. For more implementation details and endpoint variations, see the Documentation.