Old Dominion REST API: LTL Rates, Bills of Lading, Pickups and Tracking
Old Dominion Freight Line (OD) is retiring every SOAP API on www.odfl.com on
March 1, 2027, including the rate, pickup and label services the od
carrier uses. Their replacements are REST APIs on api.odfl.com, and the
od-rest carrier talks to those. It rates LTL shipments (standard, Guaranteed
by 5pm and Guaranteed by Noon), files the electronic Bill of Lading and
returns the labels and BOL as PDFs, deletes a filed BOL, schedules and
cancels pickups and tracks shipments by PRO or by reference.
Nothing changes for od until OD turns the SOAP services off. Switch your
requests to od-rest before then; see
Moving from od.
Credentials#
od-rest uses your ODFL.com website login, the same one the od carrier
takes:
| Parameter | Old Dominion value |
|---|---|
username |
Your ODFL.com username |
password |
Your ODFL.com password |
account_number |
The OD account the shipment is rated and billed against |
The same values can come from the environment as RS_OD_REST_USERNAME,
RS_OD_REST_PASSWORD and RS_OD_REST_ACCOUNT_NUMBER.
RocketShipIt exchanges the login for a session token before each request.
Tokens live for one hour, so to save a round trip call the authenticate
action, cache data.access_token until data.expires_at, and pass it back
as key. The password and the token are redacted from
meta.debug_information.
{
"carrier": "OD-REST",
"action": "authenticate",
"params": {
"username": "YOUR_ODFL_USERNAME",
"password": "YOUR_ODFL_PASSWORD"
}
}
OD prices against an account, so a login without an OD account linked to it can authenticate and track but gets "account is not associated with your username" on rates and shipments. Ask OD ([email protected]) to link the account to the login.
QA and production#
Requests go to api.odfl.com. "test": true sends them to OD's QA host
apiq.odfl.com instead. QA access is not automatic: request it from
[email protected] ("Email an Expert" on OD's
Shipping API Integrations
page). The eBOL API's own isTest flag is never sent as true; OD asks that
testing happen on the QA host.
Supported actions#
| Action | Old Dominion endpoint |
|---|---|
authenticate |
GET /auth/v1.0/token |
getallrates |
POST /rating-service/v1.0/rates |
submitshipment |
POST /BOL/v3.1/eBOL/bol-external-per-standards |
voidshipment |
DELETE /BOL/v3.1/eBOL/deleteBolPro |
createpickup |
POST /pickup/v3.0/create |
cancelpickup |
POST /pickup/v3.0/cancel |
track |
POST /tracking/v2.0/shipment.track |
Document retrieval (delivery receipts, invoices, photos), pickup status and BOL modification are not supported yet.
Describing the freight#
Each entry in packages is one handling unit (a skid, pallet, crate,
drum...). Set on each package:
weight- the handling unit's weight inweight_unit(LBorKG).freight_class- the NMFC class as a number:50,55,60,65,70,77.5,85,92.5,100,110,125,150,175,200,250,300,400or500.packaging_type- the handling unit type.SKIDwhen left out.PALLET,CRATE,DRUM,BOX,BUNDLE,CARTON,CASE,BAG,BALE,ROLL,REEL,TOTE,PAIL,LOOSEandOTHERare translated to OD's codes for whichever API the action uses (the rating and pickup APIs saySKIDandDRUM, the eBOL API saysSKDandDRM).sub_packaging_type- onsubmitshipment, the packaging of the pieces on the handling unit (BOX,CARTON...). Defaults to the handling unit type.pieces- how many pieces are on the handling unit (default 1). Printed on the BOL; not used for rating.length,width,heightinlength_unit(INorCM). Optional for rates but OD returns a note asking for them, and the rate is better with them.description- the commodity, printed on the BOL. The pickup API keeps the first 28 characters.po_number- a purchase order number printed on the BOL.hazmat- a hazmat block with onechemical_recordsentry givingid_number(UN1263),proper_shipping_name,class_division_numberandpackaging_group, plusemergency_contactandemergency_phone. TheHAZaccessorial is added for you. See Hazmat.
Shipment level:
service- leave it out ofgetallratesto rate every service. To rate one,LTL,Guaranteed by 5pmorGuaranteed by Noon(also accepted asGUARANTEED_5PMandGUARANTEED_NOON).accessorials(oraddons) - OD accessorial codes. Each API has its own list; see Accessorials.lift_gate_required- a lift gate at delivery.pickup_date- the requested pickup day (2026-12-15or20261215). Rates use it for the fuel surcharge in force that day; the BOL prints it; the pickup books it.insured_value- the declared value in whole dollars (currencyUSDorCAD).special_instructions- printed on the BOL, or sent to the driver with a pickup.
Country codes can be the two letter US, CA, MX or OD's three letter
USA, CAN, MEX. Phone numbers can carry any punctuation; OD gets the
ten digits.
Billing#
By default the account in account_number is the shipper, rated as an
outbound prepaid shipment and billed to itself. A billing entry of type
transportation changes who pays:
"billing": [
{
"type": "transportation",
"payment_type": "third_party",
"account": "THIRD_PARTY_OD_ACCOUNT",
"company": "Pays For Freight LLC",
"name": "Pat Payer",
"phone": "2145550000",
"addr1": "456 Other St",
"city": "Dallas",
"state": "TX",
"postal_code": "75201",
"country_code": "US"
}
]
payment_typethird_partyrates a third party movement billed to that account, prepaid. On the BOL the entry's address is the bill-to party.payment_typerecipientrates an inbound movement, collect, billed to the consignee's account (account, or youraccount_number).
The same billing works on getallrates, submitshipment and
createpickup. Without an address on the entry, the BOL's bill-to party is
the bill_* parameters (bill_company, bill_name, bill_addr1,
bill_city, bill_state, bill_code, bill_country, bill_phone) when
set, else the party that pays.
Rates#
{
"carrier": "OD-REST",
"action": "getallrates",
"params": {
"username": "YOUR_ODFL_USERNAME",
"password": "YOUR_ODFL_PASSWORD",
"account_number": "YOUR_ACCOUNT_NUMBER",
"shipper": "RocketShipIt",
"ship_code": "72601",
"ship_country": "US",
"to_code": "44333",
"to_country": "US",
"weight_unit": "LB",
"length_unit": "IN",
"accessorials": ["HYD"],
"packages": [
{ "weight": 200, "length": 48, "width": 40, "height": 36, "freight_class": "50", "packaging_type": "SKID", "description": "furniture" }
]
}
}
Only the postal codes and countries are required for a rate; the full addresses tighten it. One rate comes back per service OD could price, the alternates OD volunteers included:
service_code-LTL,Guaranteed by 5pmorGuaranteed by Noon.guaranteedistrueon the two guaranteed services.rate- OD's net freight charge, the amount you pay.rate_detail- the breakdown:gross_freight_charge,discount_amount,discount_percentage,discounted_freight_charge,fuel_surcharge,total_accessorial_charges, and one entry per accessorial by its code (HYD,GTD...).est_delivery_days- OD's service days between the service centers, not counting the pickup day, weekends or holidays.rate_id- OD's rate estimate number when one service was requested, otherwise OD's detail id for that service. Pass the estimate number back asrate_idonsubmitshipmentto price the BOL on that quote.package_type- OD's rate type,DEFAULT. Volume, Pallet and Security Divider rate types are not requested yet.
A service OD could not price is reported in errors with its name
("Guaranteed by Noon: ...") and left out of rates. Entries with type Note
are OD's suggestions (add dimensions...), not failures.
Creating a label and Bill of Lading#
submitshipment files the electronic BOL (the Digital LTL Council eBOL
2.1.0) and returns:
tracking_numberandshipment_id- the PRO number OD assigned.packages[]- one entry per handling unit, each with the PRO as itstracking_number. The labels are one PDF holding every handling unit's label; it is on the first package'slabeland underdocumentsasshipping_labels.documents[]- the Bill of Lading (codebill_of_lading) as a PDF and the labels (codeshipping_labels).
OD produces labels as PDF only; image_type is ignored. label_stock_type
picks the layout: left out or zebra gives one 4x6 label per page, avery
(or letter, sheet) a sheet of labels. One label per handling unit is
requested.
Add reference_value (and reference_value2, reference_value3) for BOL
numbers and po_number on the shipment or the packages for purchase orders;
all print on the BOL. rate_id from getallrates is the quote the BOL is
priced on.
Filing the BOL does not schedule the pickup; call createpickup with the
PRO in shipment_id. voidshipment with the PRO in shipment_id deletes
the BOL; OD answers "No clean data exists to delete for this pro" when there
is nothing to delete.
Pickups#
createpickup books a pickup at the pickup_* address (pickup_company_name,
pickup_contact_name, pickup_phone, pickup_email, pickup_addr1,
pickup_city, pickup_state, pickup_code, pickup_country), or at the
ship_* address when those are blank, for one shipment going to the to_*
address. Give the day as pickup_date, the time the freight is ready as
ready_time and the dock close as close_time, all in the shipper's local
time (0800, 08:00 or 08:00:00). List the handling units in packages,
or just pickup_quantity and pickup_total_weight when you do not have
them. Put the PRO from submitshipment in shipment_id to link the BOL to
the pickup; without it the driver assigns one. special_instructions goes
to the driver.
OD identifies a pickup by two numbers. The response has the pickup
number in pickup_id and the pre-PRO identifier in
reference_numbers with code PPID (plus the PRO as PRO when one is
linked). Keep both:
"data": {
"pickup_id": "888888888",
"reference_numbers": [
{ "code": "PKU", "value": "888888888" },
{ "code": "PPID", "value": "111111111" },
{ "code": "PRO", "value": "11867530911" }
]
}
cancelpickup needs both numbers: send pickup_id as
"888888888-111111111", or pickup_id "888888888" with the pre-PRO
identifier in reference_value. special_instructions is the cancel reason
OD records. An OD warning such as "eBOL data was not found for the following
PROs" comes back in errors with type Warning; the pickup is still booked.
Parcel pickups work the same way on other carriers; see Schedule a Pickup.
Tracking#
track takes the PRO in tracking_number. To track by another OD
reference, set reference_type to BOL, PO, LOAD, PKU (pickup
number) or PPID (pre-PRO identifier) and put that value in
tracking_number. OD tracks the shipment as a whole, so the events are in
activity and repeated on the single entry in packages. Each event has
OD's status in status_code (Pickup Completed, In Transit, Out for Delivery, Delivered, Delivery Confirmed, Delayed...), OD's detail in
description ("Arrived at Destination Service Center") and a coarse
status_type (PICKUP, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED,
APPOINTMENT, EXCEPTION). The shipment level fields give
estimated_delivery, delivered_time, pickup_date,
delivery_detail.received_by (who signed) and OD's reference numbers (BOL,
PO, LOAD, pickup number). The shipper's and consignee's places are shown
when the login is authorized for the account; otherwise origin and
destination are OD's service centers.
Accessorials#
OD's APIs use different code lists. accessorials takes the list for the
action, and the rating codes are translated for submitshipment so a rated
payload ships unchanged.
| Service | getallrates, createpickup |
submitshipment |
|---|---|---|
| Lift gate at delivery | HYD |
LFTD |
| Lift gate at pickup | HYO |
LFTP |
| Residential delivery / pickup | RDC / RPC |
RES / REP |
| Inside delivery / pickup | IDC / IPC |
IDL / IPU |
| Notification prior to delivery | ARN |
MNC |
| Protect from freezing | PFF |
PSC |
| Sorting or segregating | SSC |
SRT |
| Appointment delivery / pickup | CA / CP |
APTD / APTP (need appointment dates) |
| Limited access delivery / pickup | LDC / LPC |
LTDAD / LTDAP (need the access type) |
| Guaranteed by 5pm / Noon | use service |
GTD_PM / GTD_NOON |
| Hazardous material | added with hazmat |
HAZ, added with hazmat |
| Delivery / pickup photo | PVD / PVP |
PVD / PVP |
The full lists are in OD's Rating, Bill of Lading and Pickup guides.
Moving from od#
Requests for the SOAP od carrier need these changes:
"carrier": "OD-REST". Theusername,passwordandaccount_numberstay the same.getallratesreturns up to three rates instead of one. Pick theservice_codeyou want; theLTLrate is the oneodreturned. Therate_detailtypesodused are kept.submitshipmentonodbuilt the BOL through the pickup service and returned only the PRO. Onod-restit files the eBOL and returns the label and BOL PDFs, and the pickup is a separatecreatepickupcall.piecesandpackaging_typeon each package keep their meaning.track,voidshipment,createpickupandcancelpickupare new.
Errors#
OD errors land in data.errors with OD's message, prefixed with the service
they apply to on rates. HTTP 401 means the token is invalid or expired; get a
new one with authenticate. A rejected login is reported as
"authentication failed: invalid credentials". The eBOL API reports failures
on HTTP 200 inside its messageStatus block; they are mapped to errors
with OD's result code (400 formatting, 500 business logic, 1300 label
PDF not created, 1400 duplicate PRO...). OD's QA and production hosts
drop requests that carry Go's default User-Agent, so RocketShipIt always
identifies itself; if a request hangs for the full timeout, that is the
first thing to check in a custom integration.