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 in weight_unit (LB or KG).
  • 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, 400 or 500.
  • packaging_type - the handling unit type. SKID when left out. PALLET, CRATE, DRUM, BOX, BUNDLE, CARTON, CASE, BAG, BALE, ROLL, REEL, TOTE, PAIL, LOOSE and OTHER are translated to OD's codes for whichever API the action uses (the rating and pickup APIs say SKID and DRUM, the eBOL API says SKD and DRM).
  • sub_packaging_type - on submitshipment, 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, height in length_unit (IN or CM). 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 one chemical_records entry giving id_number (UN1263), proper_shipping_name, class_division_number and packaging_group, plus emergency_contact and emergency_phone. The HAZ accessorial is added for you. See Hazmat.

Shipment level:

  • service - leave it out of getallrates to rate every service. To rate one, LTL, Guaranteed by 5pm or Guaranteed by Noon (also accepted as GUARANTEED_5PM and GUARANTEED_NOON).
  • accessorials (or addons) - 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-15 or 20261215). 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 (currency USD or CAD).
  • 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_type third_party rates a third party movement billed to that account, prepaid. On the BOL the entry's address is the bill-to party.
  • payment_type recipient rates an inbound movement, collect, billed to the consignee's account (account, or your account_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 5pm or Guaranteed by Noon. guaranteed is true on 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 as rate_id on submitshipment to 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_number and shipment_id - the PRO number OD assigned.
  • packages[] - one entry per handling unit, each with the PRO as its tracking_number. The labels are one PDF holding every handling unit's label; it is on the first package's label and under documents as shipping_labels.
  • documents[] - the Bill of Lading (code bill_of_lading) as a PDF and the labels (code shipping_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:

  1. "carrier": "OD-REST". The username, password and account_number stay the same.
  2. getallrates returns up to three rates instead of one. Pick the service_code you want; the LTL rate is the one od returned. The rate_detail types od used are kept.
  3. submitshipment on od built the BOL through the pickup service and returned only the PRO. On od-rest it files the eBOL and returns the label and BOL PDFs, and the pickup is a separate createpickup call. pieces and packaging_type on each package keep their meaning.
  4. track, voidshipment, createpickup and cancelpickup are 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.