You need a reliable way to programmatically look up freight rates without guessing at prices or copy/pasting from spreadsheets. By the end of this guide, you’ll be able to call the FreightPulse freight-rates endpoint from PHP, interpret the response envelope, respect authentication and usage limits, and ship a basic integration you can evolve safely into production.
What you’ll build and what to watch out for
This walkthrough centers on fetching data from the FreightPulse freight-rates API with PHP. You will:
- Authenticate with the X-API-Key header (or proceed as a guest with limits).
- Work with the JSON envelope: {"success":true,"data":{...}}.
- Run a copy-pasteable curl request to verify connectivity.
- Parse timestamps, currency, and units from the response.
- Add a second client example (JavaScript) for cross-language parity.
Important constraints for freight developers:
- Auth: send X-API-Key: YOUR_API_KEY. Guests without a key are limited to 100 requests/day per IP.
- FreightPulse responses use an envelope with success and data. Do not assume raw arrays.
- Port congestion data (where present) is sourced from IMF PortWatch and is lagged about 6–7 days (a 7-day average vs a 90-day baseline). It is not live AIS wait time.
- Do not quote a USD freight fare from the freight-rates endpoint as “today’s price.” Treat returned values and fixtures as reference data, not a same-day live quote.
- Use only the documented fixture path and samples shown below. Do not invent parameters or interpret a sample lane as a live, binding rate.
Endpoint, auth, and envelope in one place
FreightPulse uses simple header-based authentication and a consistent response envelope. This is the minimum you need to reach the service:
- Endpoint: /api/v1/freight-rates
- Method: GET
- Auth: X-API-Key header. Value is YOUR_API_KEY for examples.
- Guest usage: Up to 100 requests/day per IP if you omit the key.
- Response shape: {"success": true, "data": {...}} (plus optional meta).
If you’re just getting started, create an account to obtain a production API key here: Register. Refer to the parameter reference as you extend beyond the sample lane: Documentation.
Quick connectivity test with curl
Run this request from your terminal to verify connectivity, headers, and the base response envelope. Replace YOUR_API_KEY with your key, or omit the header to test as a guest:
curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/freight-rates?mode=trucking&origin_zip=10001&destination_zip=90210"
This call uses the documented path and query string. Do not treat this lane (10001 → 90210, trucking) as a live quote for real-world pricing.
Understand the JSON envelope (official sample)
The official sample below demonstrates the envelope and the kinds of fields you will parse. Copy it exactly when building mocks or tests:
{
"success": true,
"data": {
"timestamp": "2026-10-22T00:00:15+00:00",
"source": "EIA (U.S. Energy Information Administration)",
"currency": "USD",
"unit": "gallon",
"data": {
"diesel": {
"national_average": 6.276,
"change_week": 0.097,
"change_percent": 1.57,
"updated_at": "2026-10-19",
"regions": {
"east_coast": 5.984,
"midwest": 6.352,
"gulf_coast": 5.903,
"rocky_mountain": 6.422,
"west_coast": 7.394,
"california": 8.242
}
},
"gasoline": {
"national_average": 4.284,
"change_week": -0.047,
"change_percent": -1.09,
"updated_at": "2026-10-19",
"grades": {
"regular": 4.284,
"midgrade": 4.915,
"premium": 5.314
}
},
"bunker_fuel": {
"available": false,
"note": "No known free source (Rotterdam/Singapore/Houston) — requires a paid provider (e.g. Ship & Bunker, Argus)."
}
},
"historical": {
"diesel_30d_avg": 6.259,
"diesel_90d_avg": 5.88,
"diesel_yoy_change": 73.4,
"gasoline_30d_avg": 4.359,
"gasoline_90d_avg": 4.213,
"gasoline_yoy_change": 41.9
}
},
"meta": {
"request_id": "72e4d78a-2f2d-4054-81d3-cd716ffe8494"
}
}
Fields you will actually use when building clients:
- success (boolean): Always check this before reading data.
- data.timestamp (ISO 8601, +00:00 timezone): Useful for cache freshness and UI staleness labels.
- data.currency and data.unit: Base currency is USD; units depend on the metric shown (the sample demonstrates “gallon”).
- data.data (object): A nested payload container. Use feature flags or keys detection to read only what your interface needs.
- data.historical (object): Aggregated reference values you can surface as context, not as same-day quotes.
- meta.request_id: Log this for traceability in error reports and support tickets.
Do not re-label fields or assume the presence of any value not shown in the official sample. If you need additional attributes, consult the Documentation.
PHP: End-to-end freight-rates lookup
The PHP example below calls the freight-rates endpoint, checks the envelope, and extracts timestamp, currency, and unit. It demonstrates header auth, safe JSON decoding, and basic error handling. Replace YOUR_API_KEY with your actual key for production use.
<?php
$apiKey = getenv('FREIGHTPULSE_API_KEY');
if (!$apiKey) {
$apiKey = 'YOUR_API_KEY';
}
$endpoint = 'https://freightpulsehq.com/api/v1/freight-rates';
$params = http_build_query([
'mode' => 'trucking',
'origin_zip' => '10001',
'destination_zip' => '90210',
]);
$url = $endpoint . '?' . $params;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . $apiKey,
'Accept: application/json',
],
CURLOPT_TIMEOUT => 20,
]);
$responseBody = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErrNo = curl_errno($ch);
$curlErr = curl_error($ch);
curl_close($ch);
if ($curlErrNo !== 0) {
// Network or TLS error
http_response_code(502);
echo json_encode([
'error' => 'Transport error',
'detail' => $curlErr,
]);
exit;
}
if ($httpCode < 200 || $httpCode >= 300) {
// Upstream returned a non-2xx code
http_response_code($httpCode);
echo json_encode([
'error' => 'Upstream HTTP ' . $httpCode,
'body' => $responseBody,
]);
exit;
}
$payload = json_decode($responseBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(502);
echo json_encode([
'error' => 'Invalid JSON',
'detail' => json_last_error_msg(),
]);
exit;
}
// Envelope check
if (!isset($payload['success']) || $payload['success'] !== true) {
http_response_code(502);
echo json_encode([
'error' => 'API indicated failure',
'body' => $payload,
]);
exit;
}
$data = isset($payload['data']) ? $payload['data'] : [];
$meta = isset($payload['meta']) ? $payload['meta'] : [];
// Extract safe, documented fields from the sample
$timestamp = isset($data['timestamp']) ? $data['timestamp'] : null;
$currency = isset($data['currency']) ? $data['currency'] : null;
$unit = isset($data['unit']) ? $data['unit'] : null;
$requestId = isset($meta['request_id']) ? $meta['request_id'] : null;
// You can pass these to your UI; do not present them as a binding, same-day freight quote.
$result = [
'timestamp' => $timestamp,
'currency' => $currency,
'unit' => $unit,
'request_id' => $requestId,
// If needed, forward the entire nested structure to downstream services:
// 'raw' => $data,
];
header('Content-Type: application/json');
echo json_encode($result);
Notes:
- The example surfaces timestamp, currency, and unit. Extend cautiously based on keys you observe and fields documented in the product docs.
- Log meta.request_id to tie client errors to upstream logs.
- If you omit the X-API-Key header, expect guest constraints: 100 requests/day per IP. Defend against surprise throttling with application-level caching (see below).
JavaScript fetch example
This Node/Browser-compatible snippet calls the same endpoint and extracts the same core fields. If you run it in Node, set FREIGHTPULSE_API_KEY in your environment or hardcode YOUR_API_KEY while testing.
async function getFreightRates() {
const apiKey = process.env.FREIGHTPULSE_API_KEY || 'YOUR_API_KEY';
const url = 'https://freightpulsehq.com/api/v1/freight-rates?mode=trucking&origin_zip=10001&destination_zip=90210';
const res = await fetch(url, {
method: 'GET',
headers: {
'X-API-Key': apiKey,
'Accept': 'application/json'
}
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Upstream HTTP ${res.status}: ${text}`);
}
const payload = await res.json();
if (!payload || payload.success !== true) {
throw new Error('API indicated failure or missing success flag');
}
const data = payload.data || {};
const meta = payload.meta || {};
// Extract safe fields from the official sample
const summary = {
timestamp: data.timestamp || null,
currency: data.currency || null,
unit: data.unit || null,
request_id: meta.request_id || null
};
return summary;
}
getFreightRates()
.then(summary => {
console.log('FreightPulse summary:', summary);
})
.catch(err => {
console.error('Error:', err.message);
});
Practical integration details that save time
Units and currency:
- Base currency is USD (data.currency = "USD" in the sample). Do not convert or re-denominate unless your UI explicitly indicates it.
- Units reflect the metric in view (sample shows “gallon” for fuel). Surface units alongside values in any UI labels to avoid ambiguity.
Timestamps and timezone:
- ISO 8601 with explicit offset (e.g., 2026-10-22T00:00:15+00:00). Treat as UTC when scheduling refresh jobs and computing staleness.
- Display an age badge in your UI (e.g., “Updated 3 days ago”) rather than promising real-time values.
Caching:
- Cache responses by the full URL (including query string) and the presence/absence of X-API-Key, for at least a few minutes while you validate integration.
- Use the timestamp to decide whether to refresh in the background; avoid blocking the page for users.
- Store meta.request_id with your cache entry for traceability.
Usage limits:
- Guests: 100 requests/day per IP. If you’re experimenting without a key, keep an eye on request volumes in CI pipelines and shared proxies.
- Add exponential backoff on 5xx, and gate aggressive retries behind a circuit breaker. For 4xx, surface a helpful message and log the request_id.
Pagination and field availability:
- If a response can span multiple pages or datasets, the documentation will describe the mechanism. Do not assume cursor or page parameters unless you see them in the docs.
- Always null-check optional fields and prefer feature detection (e.g., if (data.historical) ...) over hardcoded assumptions.
Non-trading days equivalent:
- Freight and supporting indices may update on business schedules. Design your UI for periods with no fresh ticks (weekends/holidays) and continue to show the last-known timestamp instead of hiding data.
On port congestion and why it’s not a “live wait time”
If you display port congestion metrics alongside freight rates, annotate them clearly. FreightPulse congestion references IMF PortWatch and is lagged about 6–7 days, using a 7‑day average versus a 90‑day baseline. It is not sourced from live AIS vessel waits.
- Label: “IMF PortWatch (lagged ~6–7 days), 7‑day avg vs 90‑day baseline.”
- Do not present this as live queue length or real-time berth delay.
- Keep the congestion timestamp near the value so operators understand staleness.
Presenting rates responsibly
Do not quote any USD freight rate as “today’s price” from the freight-rates endpoint. Treat responses as reference data or inputs to your internal calculators. Where you need a binding price, implement a separate quoting workflow that stores terms, validity windows, and vendor/contract attribution outside of the read-only FreightPulse lookup.
- Show timestamped context (currency, unit, timestamp) instead of a promise of live availability.
- For any lane you surface, include disclaimers that values are indicative and should not be treated as same-day binding quotes.
- Avoid “autorefresh” dashboards that imply second-by-second updates; prefer manual refresh controls with last-updated badges.
Troubleshooting and observability
Quick checks when something looks off:
- Headers: Ensure X-API-Key is present and correct. A missing key drops you into guest mode with tighter limits.
- Envelope: Always test payload.success === true before reading data. If false, capture the full body for review.
- Logging: Persist meta.request_id from every call. It shortens support cycles.
- Staleness: Verify data.timestamp. If it’s older than your SLA for the view, render an info badge and trigger a background refresh.
Validation and safe rollout checklist
- Replicate the sample curl call and confirm an HTTP 200 and a JSON object with success: true.
- In PHP, assert non-empty timestamp, currency, and unit fields before rendering any UI that references them.
- Attach request_id to all error logs and to user-visible error screens in admin tools.
- Implement a small in-memory or Redis cache keyed by the full request URL to dampen duplicate calls.
- Set a cron or queue worker to refresh high-traffic lanes on a cadence appropriate to your UI, rather than refreshing on every page load.
FAQ
Can I call the freight-rates endpoint without an API key?
Yes. Guests are allowed 100 requests per day per IP. Add the X-API-Key header when you’re ready to exceed that or move to production.
Is the port congestion metric live?
No. FreightPulse port congestion references IMF PortWatch and is lagged roughly 6–7 days (7‑day average vs 90‑day baseline), not live AIS waits.
Can I present the returned USD values as a same-day quote?
No. Do not quote a freight-rates USD fare as today’s price. Treat responses as reference data and always display timestamps.
What response shape should I code against?
Use the envelope: success (boolean), data (object), and optional meta (object). Null-check fields and avoid assuming undocumented attributes.
Where do I find parameters and extended fields?
See the product docs for the latest query parameters and field definitions: Documentation.
Ready to integrate with a proper API key and ship your first freight lookup safely? Create your account here: Register. Keep the envelope contract, respect the lag on congestion metrics, and avoid claiming same-day quoted prices, and you’ll have a dependable pipeline from sandbox to production.