Skip to main content

Quote API

The Quotes API allows you to retrieve shipping quotes from Starboard, record the outcome of pushing them to your system, and set which customer a quote belongs to.

Base Path: /api/v3/quotes


Types​

Quote Type​

Represents a shipping quote.

PropertyTypeDescription
idstringQuote ID
company_idstringCompany ID that owns this quote
rfq_idstringAssociated RFQ ID
quote_numberstringHuman-friendly identifier (e.g., Q-000001)
statusQuoteStatus enumQuote status
owner_user_idstringID of the user who owns this quote
customer_company_idstringID of the customer company. Absent when the quote has no customer. Change it by the company's external_id with Update Quote Customer.
incotermstringIncoterm (e.g., FOB, CIF)
commoditystringCommodity description
effective_datestringDate in YYYY-MM-DD format
expiring_datestringDate in YYYY-MM-DD format
display_currencystringCurrency for display (e.g., USD)
exchange_ratesobjectThe exchange rates the quote is priced with, as { "<from>": { "<to>": rate } }: an amount in <from> times rate is the amount in <to> (for example, { "USD": { "CAD": 1.35 } }). When only the opposite pair is listed, Starboard uses 1 / rate. An older quote may carry a flat map instead (such as { "USD": 1, "CAD": 1.35 }); the API doesn't convert with that form.
file_pathstringWhere Starboard stores the quote's PDF: a storage path, not a URL. To download the PDF, use Download Quote Document or Read Quote's download_url.
generated_atstringWhen the quote PDF was generated
created_atstringDate in ISO 8601 format
updated_atstringISO 8601 timestamp that advances when the quote, or any lane, leg, rate, rate item, or cargo under it, is saved. Recording a push outcome or an external_quote_number via POST /push_status does not change it. It is a change signal, not a guarantee — see Detecting changed quotes.
sent_atstringWhen the quote was sent to customer
lanesLane[]The quote's lanes, with their legs, rates and cargo. Read prices from here.
routesRoute[]A summary of each lane without prices, kept for older integrations: one per lane, with the same id. Read prices from lanes.
last_push_statusstringOptional. One of success, failed, in_progress. Absent or null when no push has ever been recorded for this quote; treat both alike. Set via POST /api/v3/quotes/{quote_id}/push_status.
last_push_errorobjectOptional. Free-form JSON payload describing the failure. Present only when last_push_status = "failed". Shape is caller-defined — typical fields are code (string), message (string), and an optional raw object with the vendor's original payload.
last_push_integrationstringOptional. The integration_type of the last Record Push Status call, such as cargowise_trm or external_quote_sync. Set alongside the other last_push_* fields on every POST /push_status call.
last_push_atstringOptional. ISO 8601 timestamp of the last push outcome. Set by Starboard when the call is received; it can't be backdated.
external_quote_numberstringOptional. The quote's number in the integrator's system (for example, the quote number CargoWise assigned). Absent or null until one is reported via POST /push_status; treat both alike.

There is no quote-level list of charges. Your company's own charges (from Starboard's settings) are rate items with charged_by: "company"; see Who bills a line.

Detecting changed quotes​

updated_at advances when the quote, or any lane, leg, rate, rate item, or cargo under it, is saved — including lanes being added or removed. To keep a copy in sync, store the updated_at you last processed for each quote and re-process the quote whenever the API returns a different value.

  • Compare for any difference, not "later". The timestamp is taken when a change starts, not when it is committed, so two overlapping changes can commit out of order and a quote's updated_at can occasionally move slightly backwards. Treating any difference from your stored value as a change catches this. Don't subtract a time buffer from a per-quote value — an unchanged quote would then look changed on every poll.
  • Your own push reports don't count. Calls to POST /push_status — including ones that set external_quote_number — leave updated_at unchanged, so recording a push never makes the quote look modified, and never sends a webhook event.
  • It can advance without a visible change. Some internal writes (for example, the quote being re-saved or its PDF regenerated) move updated_at even when nothing in the payload differs. Compare the payload if you need to know exactly what changed.
  • It's a change signal, not a guarantee. A few values are worked out when you read the quote rather than stored with it — for example, rate totals that depend on your company's rate-item settings, and each rate item's cargowise_charge_code — and a reference such as a rate's vendor_rate_doc_id is cleared when the referenced record is deleted. Neither moves updated_at. If you need exact parity, periodically re-fetch the quotes you still act on (for example, open ones) and compare payloads.
  • Leave the signed links out of a comparison. Each rate's vendor_email_url, and Read Quote's download_url, are signed again on every read, so they differ every time even when nothing changed.
  • Ask only for what changed. List Quotes with updated_since returns just the quotes changed since a time you choose: see Incremental sync with updated_since. Webhooks send you the changes instead.

Incremental sync with updated_since​

List Quotes with updated_since returns only the quotes whose updated_at is at or after the time you give, oldest change first. To keep a copy in sync, track a mark: the highest updated_at you've processed.

  1. Ask for the window. Request updated_since set to your mark minus 2 minutes, with any filters you need (for example status=win). For the first sync, use a time before your first quote, such as 2000-01-01T00:00:00Z.
  2. Walk it to the end. Follow cursor from page to page, sending the same filters, updated_since and limit with each cursor, until the last page returns "cursor": "".
  3. Skip what you've already processed. Process each quote unless you've already processed that id with that updated_at. The overlap returns the last 2 minutes of the previous window again, and a quote that changes after the walk has read it comes back near the end with its new updated_at.
  4. Move your mark. Save the highest updated_at you've processed. Quotes come oldest change first, so you can save it after every page: a walk that's interrupted picks up from there next time.
  5. Poll at most once a minute.
The cursor is for one walk only

cursor only moves you through the pages of one walk, and the walk ends when a page returns "cursor": "". Don't keep a cursor between polls: start every poll as a new walk, with updated_since set to your mark minus 2 minutes and no cursor.

Why the overlap: updated_at is the time a change started, not the time it was saved, so a change can commit slightly in the past, after your walk has passed its updated_at. The 2-minute overlap picks it up on your next poll. It applies to the window you ask for; per quote, keep comparing for any difference, as above. Unlike List Companies, which keeps its creation-date order, List Quotes with updated_since returns quotes in the order they changed, so the highest updated_at you've processed is a safe mark.

What the window doesn't tell you:

  • Deleted quotes. A deleted quote stops appearing; nothing reports the deletion. See Keeping a copy of won quotes.
  • Quotes that leave your filter. With a filter such as status=win, a quote that changes to another status no longer matches, so the window doesn't show that change. Leave status out to see every change, or see Keeping a copy of won quotes.
  • Changes that don't move updated_at, listed above: your own push reports, and values worked out when you read the quote.

For a worked example, see the examples under List Quotes.

Keeping a copy of won quotes​

  1. Sync status=win incrementally, as in Incremental sync with updated_since. Newly won quotes and changes to won quotes come back; a status change advances updated_at.
  2. Booking statuses. If your company books air freight through Starboard, a won quote moves on to booking_requested, then booking_confirmed or booking_rejected (see QuoteStatus). List Quotes takes one status per request, so sync each of those statuses the same way, with a separate mark for each: a mark taken from one status's walk doesn't cover another's.
  3. Quotes that leave these statuses. A won quote that's reopened, lost or deleted stops matching, so the incremental sync doesn't report it. Once a day, or as often as you need, walk each of these statuses in full, with an updated_since before your first quote (such as 2000-01-01T00:00:00Z) so that quotes changing during the walk move to the end instead of being skipped. A stored quote missing from every walk has changed to another status, or been deleted. Read it with Read Quote: 404 means it was deleted (for example, along with its customer company; see Delete Company); otherwise its status says where it went. Webhooks report a followed quote's status changes as they happen.
  4. Quotes not yet in your system. status=win&pushed=false lists the won quotes whose last push isn't a success, or that have no external_quote_number (see List Quotes).

Lane Type​

Represents a lane in V3 native format.

PropertyTypeDescription
idstringLane ID. The matching entry in routes has the same id.
rfq_numberstringAssociated RFQ number
primary_service_typestringPrimary service type (see ServiceType enum)
sort_ordernumberDisplay order
originstringThe lane's origin as saved on the lane (usually a port code), when set. It can be absent or differ from the legs: read locations from legs.
destinationstringThe lane's destination as saved on the lane, when set. Same caveat as origin.
legsLeg[]The lane's legs, each with its rate options
cargoCargo[]The lane's cargo rows, as a flat list. Contents of a pallet or crate point at it through parent_id.
cargo_v2CargoV2The same cargo already grouped: each pallet or crate with its contents inside, plus lane totals. Read this rather than matching parent_ids yourself. Sent on lanes only, not on routes.
created_atstringCreation timestamp

A lane carries no shipment type, container summary, pickup or delivery address, or via ports. Read them from the lane's legs and cargo, or from the route with the same id in routes.

Route Type​

Represents a route in V2-compatible format.

PropertyTypeDescription
idstringRoute ID: the same as the lane's id in lanes
rfq_numberstringAssociated RFQ number
sort_ordernumberAlways 0. Routes are in lane order.
shipment_typestringShipment type (ShipmentType enum): FCL, LCL or AIR. On a lane with only trucking, the lane's service type as is (for example, origin_drayage). Absent when the lane has no service type.
equipment_typestringFCL only: the first container type on the lane (e.g., 40HC)
equipment_typesEquipmentType[]FCL only: each container type on the lane with its count
quantitynumberFCL only: total number of containers on the lane; 1 when the lane's cargo has no counts. Absent, like equipment_type and equipment_types, when the lane has no cargo rows.
cargoCargo[]LCL and air only: the lane's cargo rows, as in lanes[].cargo
pickup_locationstringPickup address from the origin drayage or cartage leg (that leg's location when it has no address)
originstringOrigin port. Ocean: a display string, Name, Subdivision, Country (e.g., Shanghai, SH, China). Air: the airport code (its name when it has no code).
destinationstringDestination port, in the same form as origin
delivery_locationstringDelivery address from the destination drayage or cartage leg (that leg's location when it has no address)
origin_via_portstringVia port near origin, as a display string (not a code)
destination_via_portstringVia port near destination, as a display string (not a code)
created_atstringCreation timestamp
route_legsRouteLeg[]Route legs. They carry no rates.
Ports and codes

On ocean routes, origin, destination and the via ports (on the route and its route legs) are display strings, not codes. Only air routes carry codes, and only in origin and destination. For UN/LOCODEs, read lanes[].legs[].origin.locode and lanes[].legs[].destination.locode (see Location Type).

RouteLeg Type​

Represents a single leg of a route in V2-compatible format.

PropertyTypeDescription
idstringLeg ID
service_typestringService type (see ServiceType enum)
sort_ordernumberAlways 0. Route legs are in leg order.
ratesnullAlways null. Rates are on lanes[].legs[].
if origin_drayage or origin_cartage
originstringThe route's origin
pickup_locationstringPickup address
if destination_drayage or destination_cartage
destinationstringThe route's destination
delivery_locationstringDelivery address
if ocean_fcl, ocean_lcl, or air
originstringOrigin port, in the same form as the route's origin
destinationstringDestination port, in the same form as the route's destination
origin_via_portstringVia port near origin, as a display string (if applicable)
destination_via_portstringVia port near destination, as a display string (if applicable)

Leg Type​

Represents a single leg within a lane (V3 native format).

PropertyTypeDescription
idstringLeg ID
service_typestringService type (see ServiceType enum)
sort_ordernumberDisplay order
excluded_by_incotermbooleantrue when the quote's incoterm puts the whole leg outside the customer's scope, so nothing on it is billed: every rate item on the leg has is_included_in_total: false and exclusion_reason: "incoterm", and the leg's rates have total_buy_rate and total_sell_rate of 0. Sent only when true.
originLocationOrigin location
origin_addressAddressOrigin address details (for drayage/cartage)
destinationLocationDestination location
destination_addressAddressDestination address details (for drayage/cartage)
origin_viaLocationVia port near origin
destination_viaLocationVia port near destination
ratesRate[]Every rate option on this leg, not only the quoted ones. See Which rates are quoted.
created_atstringCreation timestamp

Via ports and pickup and delivery addresses also appear as plain strings (origin_via_port, pickup_location and so on) on each route in routes, and on the route legs where they apply. A leg has no transit time of its own: it's on each rate, as transit_time_days.

Which rates are quoted​

A leg's rates lists every rate option on the leg, in sort_order order (oldest first when two share a sort_order). The ones on the quote are the rates with selected_at set; if no rate on the leg has it, the first rate in rates is the quoted one.

When a leg has more than one selected rate, they're usually alternatives offered to the customer: don't add them up. The exception is an ocean_fcl, origin_drayage or destination_drayage leg where each selected rate prices different container types (for example, the 20GP from one carrier and the 40HC from another): together they make one price. Selected rates there are alternatives as soon as two of them price the same container type, or one of them names no container type. A rate's container types are the equipment_type values on its rate items, leaving out charged_by: "company" lines, plus the container types in its rate.equipment_rates.

A quote's price​

The payload has no total for the quote or for a lane. Work prices out from the rates:

  • A rate: total_buy_rate and total_sell_rate are its all-in totals in total_currency (the quote's display currency), counting only the charges the customer pays. On an FCL or drayage rate they cover every container type the rate prices.
  • A leg: take its quoted rates (see Which rates are quoted). If they make one price, add their totals; if they're alternatives, each one is a separate price for the leg. A leg with excluded_by_incoterm: true adds nothing: its rates' totals are 0.
  • A lane: add up its legs. When a leg has alternatives, the lane has one price for each of them (one per combination when several legs have alternatives).

A rate can come without total_buy_rate and total_sell_rate, mostly on FCL and drayage legs (see Rate Type). The API then has no total for it: its lines are sent as stored, can be in several currencies, and may not add up to what the quote document shows. Treat that rate's price as unknown, or convert each billed line (is_included_in_total: true) to the display currency with the quote's exchange_rates and add them up, knowing the result may differ from the quote document.

Location Type​

Represents a port or city location.

PropertyTypeDescription
locodestringThe port's UN/LOCODE (e.g., USNYC). On air legs, the airport's three-letter code (e.g., LHR).
namestringLocation name (e.g., New York)
countrystringCountry, in no fixed form: on air legs usually the two-letter ISO 3166 code, on other legs usually the country's name
country_codestringISO 3166 two-letter country code (e.g., US). Sent only on some airports picked by hand in Starboard.
subdivisionstringState/Province
locodesstring[]All the codes for the location, for a city with several ports or a choice of airports

To get a location's country code, use country_code when it's present, else country when it's two letters, else the first two letters of locode (a UN/LOCODE starts with its country code; this doesn't apply on air legs, where locode is an airport code). Otherwise country is a name: map it to a code yourself.

Other fields can appear on a location; ignore the ones you don't use.

Cargo Type​

Represents cargo details.

PropertyTypeDescription
idstringCargo ID
cargo_typestringOne of pallet, box, crate, package, container, weight, volume or cargo. A weight or volume row is a lane total given only as a weight or a volume. cargo is a legacy wrapper row that groups other rows; cargo_v2 leaves it out.
package_codestringCargoWise package code: PLT (pallet), CTN (box), CRT (crate), PKG (package, or a weight / volume lane total). Absent for containers and cargo wrapper rows. Use this rather than mapping cargo_type yourself: the box and package codes may still change.
parent_idstringThe ID of the row's parent. A row is contents only when its parent is a pallet or crate on the same lane; then its pieces is the count on one parent unit (for example, boxes per pallet). Other rows can still carry a parent_id (pointing at a legacy cargo wrapper, at a pallet since changed to a box, or at a row that no longer exists); they are top-level items. The lane's cargo_v2 applies this rule for you.
container_typestringContainer type (e.g., 40HC)
piecesnumberNumber of pieces
weight_kgnumberWeight in kilograms of one unit, as entered. On a weight or volume row, it's the lane total instead.
volume_cbmnumberVolume in cubic meters of one unit, as entered. On a weight or volume row, it's the lane total instead.
chargeable_weight_kgnumberChargeable weight in kg
dimensionsnumber[]Dimensions of one unit, [L, W, H], in centimeters
created_atstringCreation timestamp

For totals, read cargo_v2 (total_weight_kg and total_volume_cbm on each item, and the lane's totals) rather than adding up rows yourself. Starboard's totals aren't always weight_kg or volume_cbm times pieces:

  • A row with no pieces (or 0) counts as one unit.
  • For contents of a pallet or crate, pieces is per parent unit, so the row's total is also multiplied by the parent's count.
  • A pallet or crate with contents weighs what its contents weigh, plus a crate's tare_weight_kg; its own weight_kg isn't used.
  • Volume is worked out from dimensions when they're filled in (for a pallet or crate with contents, from the contents), and otherwise usually from volume_cbm. A pallet without dimensions can take a standard pallet size instead.

CargoV2 Type​

A lane's cargo, grouped: each pallet or crate with its contents inside it, and lane totals worked out with the same cargo maths as Starboard's quote totals. Only pallets and crates hold contents; a row that points at any other parent, or at one that isn't on the lane, is listed as a loose item.

PropertyTypeDescription
schema_versionnumberVersion of this shape. Currently 1.
itemsCargoNode[]Top-level cargo: pallets, crates, loose boxes and packages, containers, and weight / volume lane totals. cargo wrapper rows are left out.
totalsobjectLane totals: pieces, pallets, crates, weight_kg and volume_cbm. pieces counts top-level units (a pallet holding 20 boxes counts once) and skips weight / volume rows. pallets and crates count pallet and crate rows only; a saved pallet count is in palletization, not here. weight_kg and volume_cbm apply the lane's Cargo Totals override the way the quote document does (a floor for air, a replacement otherwise), so they can differ from the sum of items.
palletizationobject or nullThe lane's saved pallet count, when it has one (from Palletize, or typed in by hand): pallet_count, and explanation (the palletize note saved on the quote, or null; it can be stale if the count was later typed by hand). These pallets aren't listed in items. null when the lane has no saved pallet count.
warningsstring[]One sentence per row listed as a loose item because its parent isn't on the lane, or is something other than a pallet, crate or cargo wrapper. Usually empty.

CargoNode Type​

One item in a CargoV2 tree.

PropertyTypeDescription
idstringThe cargo row's ID (the same as the flat Cargo row)
cargo_typestringAs on the Cargo row
package_codestring or nullPLT, CTN, CRT or PKG; null for containers
container_typestring or nullContainer type (e.g., 40HC)
quantitynumber or nullCount as entered. For contents, the count on one parent unit (for example, boxes per pallet).
total_quantitynumber or nullFor contents, quantity × the parent's quantity; for a top-level item, its own count. null for a weight / volume lane total.
weight_kgnumber or nullWeight per unit as entered; for a weight / volume lane total, the lane total
volume_cbmnumber or nullVolume per unit as entered; for a weight / volume lane total, the lane total
dimensionsnumber[] or nullDimensions [L, W, H]
total_weight_kgnumberThe item's total weight in kg, across all its units. For a pallet with contents, that's the contents' weight (the pallet's own weight_kg isn't added); a crate adds its tare_weight_kg.
total_volume_cbmnumberThe item's total volume in cubic meters, across all its units
tare_weight_kgnumber or nullTare weight in kg
is_pre_palletizedboolean or nullPre-palletized flag, as on the cargo row
is_pre_packedboolean or nullPre-packed flag, as on the cargo row
is_stackableboolean or nullStackable flag, as on the cargo row
is_hazmatboolean or nullHazmat flag, as on the cargo row
is_fragileboolean or nullFragile flag, as on the cargo row
notesstring or nullNotes on the cargo row
contentsCargoNode[]What sits on this pallet or crate; empty for anything else

Rate Type​

Represents a rate option for a leg.

PropertyTypeDescription
idstringRate ID
vendor_company_idstringVendor company ID
vendor_company_namestringVendor company name
carrier_namestringCarrier name
carrier_idstringCarrier ID
is_ad_hocbooleanWhether this is an ad-hoc rate
vendor_rate_doc_idstringSource rate document ID: the rate sheet the rate came from, or a reply to a rate request (including one that couldn't be matched to a vendor)
vendor_email_urlstringSigned link to the vendor's rate email (.eml) behind this rate, for attaching to your own records (for example, a CargoWise quote's eDocs). A rate taken from a reply to a rate request links to that reply. Valid for 1 hour: download the file when you read the quote rather than storing the link. Absent when the rate has no email, or when the link couldn't be made, so an absent link doesn't prove there is no email.
rateobjectThe rate's price as saved. See The rate field.
fscnumber or objectFuel surcharge
currencystringCurrency code
total_buy_ratenumberAll-in buy total for the rate, in total_currency. Omitted when the rate has no saved total. On air, LCL and cartage rates Starboard works a missing total out when you read the quote, when it can; on FCL and drayage rates it doesn't, so there the total can stay absent.
total_sell_ratenumberAll-in sell total for the rate, in total_currency. Omitted in the same cases as total_buy_rate.
total_currencystringCurrency of total_buy_rate / total_sell_rate (the quote's display currency)
markup_percentagenumberRate markup as a percentage: (total_sell_rate - total_buy_rate) / total_buy_rate * 100. Omitted when buy is zero or totals are absent.
markup_flatnumberRate markup as an absolute amount: total_sell_rate - total_buy_rate, in total_currency. Omitted when totals are absent.
effective_datestringRate validity start date
expiring_datestringRate validity end date
transit_time_daysobjectTransit time range in days: { "min": 20, "max": 25 }. Absent when unknown.
selected_atstringWhen the rate was selected for the quote. Absent on rates that weren't; see Which rates are quoted.
sort_ordernumberDisplay order
rate_itemsRateItem[]The rate's line items — the base freight line plus accessorials
created_atstringCreation timestamp

The rate-level totals (total_buy_rate, total_sell_rate) are expressed in the quote's display currency (total_currency), whereas each rate item's buy_rate / sell_rate are in that item's own currency. Air, LCL and cartage rates with totals (see Lines of an air, LCL or cartage rate), and usually FCL and drayage rates with a markup (see Lines of an FCL or drayage rate with a markup), send their billed lines' buy_rate and sell_rate in the display currency, adding up to the totals. Otherwise the lines are sent as stored: they can be in different currencies and need not add up, so use total_buy_rate / total_sell_rate. To add rates up into a lane's price, see A quote's price.

The rate field​

rate is the rate's price as Starboard saved it: a free-form object whose keys depend on where the rate came from (for example amount, a price per container type such as "40HC": 1200, or per_kg / per_cbm), and which can hold keys for Starboard's own use. Don't read prices from it: use total_buy_rate / total_sell_rate and the rate items.

One key is useful. On an FCL or drayage rate priced per container type, rate.equipment_rates maps each container type (for example 40HC) to an object with that container's buy_amount and sell_amount as saved and, when the containers come from different vendors, its vendor_company_id, vendor_company_name and carrier_name. It can carry other keys too, and a default key isn't a container type. Use it to see which container types a rate prices (see Which rates are quoted) and who supplies each one.

Rate Item Type​

A single line item on a rate — the base freight line or an accessorial.

PropertyTypeDescription
idstringRate item ID: a UUID, except on the freight line the API adds (see Lines of an air, LCL or cartage rate)
namestringDisplay name
surcharge_typestringType of line item
is_base_freightbooleanWhether this is the primary freight line (ocean/air/lcl freight, drayage/cartage base). Derived from the line name. Note: a multi-equipment FCL rate has one base line per container type, so this is not unique per rate.
unit_ratenumberPrice per usage unit (per container, per kg, per CBM…). Distinct from buy_rate/sell_rate, which are line totals. On the billed lines of an FCL or drayage rate with a markup, the per-unit buy in the quote's display currency; see Lines of an FCL or drayage rate with a markup. On an air, LCL or cartage rate it stays in the charge's original currency, even when currency says the display currency; see Lines of an air, LCL or cartage rate.
rate_percentagenumberPercentage rate
user_rate_overridenumberUser-overridden rate. On an FCL or drayage rate with a markup, the freight line's per-unit sell in the quote's display currency; see Lines of an FCL or drayage rate with a markup. On an air, LCL or cartage rate it stays in the charge's original currency, like unit_rate.
buy_ratenumberResolved buy total for this line (usually unit_rate × quantity, after usage/charge caps; on an FCL or drayage rate with a markup, see Lines of an FCL or drayage rate with a markup), in currency. Omitted when not yet computed.
sell_ratenumberResolved sell total for this line, in currency. Omitted when not yet computed.
currencystringCurrency code
usage_unitstringBilling unit as saved, free text (e.g., eq, kg, W/M); spelling and case vary
equipment_typestringApplicable equipment type
is_visiblebooleanWhether visible to customer
is_included_in_totalbooleantrue when the customer pays this charge: it counts toward the rate's total and is billed on the customer's quote document. false otherwise, and exclusion_reason says why. Accounts for the quote's incoterm, not just the Include tick. It's about billing the customer, not about your cost: a charge outside the customer's incoterm can still be a cost to you.
is_removedbooleanAlways false: charges deleted in Starboard are no longer sent. Kept so existing readers don't break.
exclusion_reasonstringWhy the charge isn't billed: optional, incoterm or included_in_freight. Present only when is_included_in_total is false. See Unbilled charges.
sort_ordernumberDisplay order
created_atstringCreation timestamp. Absent on the freight line the API adds.
charged_bystringWho bills this line: always one of vendor, company or unknown. See Who bills a line.
vendor_company_idstringThe company that bills this line. Present only on vendor and company lines, and not on every vendor line; see Who bills a line.
vendor_company_namestringThe vendor's name as saved on the rate (for a spot rate, the carrier's name). vendor lines only, and not on every one; see Who bills a line.
carrier_namestringThe carrier, when the rate names one. vendor lines only, and not on every one; see Who bills a line.
cargowise_charge_codestringThe line's charge code in your own CargoWise. Only for companies with the CargoWise integration turned on, on quotes in the statuses listed under CargoWise charge codes.

Unbilled charges​

Rate items are itemized: every charge is its own line, including charges the customer's quote document folds into the freight line. Those have is_visible: false but are still billed, so don't drop them; is_visible only says whether the document shows the charge on its own line or inside the freight. Which charges are billed matches the quote document:

  • Charges deleted in Starboard are not sent.
  • Charges with no price at all (no rate, percentage, amount, override or minimum charge, and no tiers; zero counts as no price) are not sent. The quote document never shows them.
  • Charges the customer doesn't pay are sent with is_included_in_total: false and an exclusion_reason. When more than one reason applies, incoterm wins, then included_in_freight, then optional:
exclusion_reasonMeaning
optionalInclude is unticked, or it's a vendor charge that applies only under some condition. The quote document lists it under "Additional charges if applicable".
incotermOutside the customer's scope under the quote's incoterm. When a whole leg is outside it, the leg has excluded_by_incoterm: true.
included_in_freightThe rate sheet says the freight already covers this charge.

Who bills a line​

Every rate item says who bills it, so you can set the creditor on each charge line (for example, in CargoWise). charged_by is always one of three values:

charged_byLinesvendor_company_id
vendorThe rate's own lines: the freight, the vendor's and carrier's surcharges, and extras from the vendor's list of optional charges.The vendor's company. On an FCL or drayage rate priced per container by different vendors, each line names its own container's vendor. Absent when there is no company to name: spot and contract-API rates (they name only the carrier), a rate-request reply that couldn't be matched to a vendor, a manual rate saved without a vendor, or a line no container names on a rate whose containers have different vendors.
companyYour own charges from Starboard's settings (global charges such as a handling or e-manifest fee).Your own company: the quote's company_id.
unknownCharges a user typed in by hand. Starboard doesn't record who bills them.Absent. Leave the creditor blank.

On a line that no container names, on a rate whose containers have different vendors, vendor_company_name is absent too, and carrier_name is absent when their carriers differ.

To get the vendor's code in your system, read the company with Read Company and use its external_id. If that returns 404, leave the creditor blank. Don't look up the vendor_company_id of a company line: it's your own company, not a CRM record.

Example (an excerpt: each line also carries the other Rate Item fields, such as unit_rate, usage_unit and created_at):

"rate_items": [
{
"id": "00000000-0000-0000-0000-000000000101",
"name": "Ocean Freight",
"is_base_freight": true,
"buy_rate": 1500,
"sell_rate": 1700,
"currency": "USD",
"is_visible": true,
"is_included_in_total": true,
"is_removed": false,
"sort_order": 0,
"charged_by": "vendor",
"vendor_company_id": "00000000-0000-0000-0000-000000000010",
"vendor_company_name": "Example Transport",
"carrier_name": "MSC"
},
{
"id": "00000000-0000-0000-0000-000000000102",
"name": "Overweight",
"is_base_freight": false,
"buy_rate": 150,
"sell_rate": 150,
"currency": "USD",
"is_visible": true,
"is_included_in_total": false,
"is_removed": false,
"exclusion_reason": "optional",
"sort_order": 1,
"charged_by": "vendor",
"vendor_company_id": "00000000-0000-0000-0000-000000000010",
"vendor_company_name": "Example Transport",
"carrier_name": "MSC"
},
{
"id": "00000000-0000-0000-0000-000000000103",
"name": "Handling Fee",
"is_base_freight": false,
"buy_rate": 75,
"sell_rate": 75,
"currency": "USD",
"is_visible": true,
"is_included_in_total": true,
"is_removed": false,
"sort_order": 2,
"charged_by": "company",
"vendor_company_id": "00000000-0000-0000-0000-000000000001"
},
{
"id": "00000000-0000-0000-0000-000000000104",
"name": "Customs Clearance",
"is_base_freight": false,
"buy_rate": 120,
"sell_rate": 150,
"currency": "USD",
"is_visible": true,
"is_included_in_total": true,
"is_removed": false,
"sort_order": 3,
"charged_by": "unknown"
}
]

Lines of an air, LCL or cartage rate​

On an air, ocean_lcl, origin_cartage or destination_cartage leg, each rate that has total_buy_rate and total_sell_rate has its lines rewritten so the billed ones add up to those totals. (On a leg with excluded_by_incoterm: true, and on a rate without totals, the lines are sent as stored.)

  • Every line's currency is the quote's display currency. The buy_rate and sell_rate of lines other than freight are converted to it with the quote's exchange_rates. unit_rate and user_rate_override are not converted: they stay in the charge's original currency.
  • The freight line's buy_rate and sell_rate are the balance: the rate's total minus the other billed lines. Lines with is_included_in_total: false are converted too but don't count.
  • When the rate has a markup set in Starboard, the whole markup is on the freight line: every other line, billed or not, has sell_rate equal to its buy_rate (0 when it has no buy_rate).
  • When the rate's freight isn't saved as a line of its own, the API adds one at the start of rate_items. Its id is synth-freight-<rate id> (not a UUID), name is Air Freight on air rates and Ocean Freight on the others, surcharge_type is flat, sort_order is -1, and it has no created_at. Its usage_unit is KG, TON or CBM, with unit_rate taken from the rate's price, or both are absent.
Lines Starboard couldn't convert

If exchange_rates has no rate between a charge's own currency and the display currency, that line's buy_rate and sell_rate are sent unconverted, yet its currency still names the display currency. The freight line, being the balance, is then off by the difference; the rate's totals are not affected.

The payload doesn't say when this happened, because a line's original currency isn't sent. A warning sign is a rate whose own currency differs from the display currency while exchange_rates pairs the two in neither direction, or an exchange_rates that isn't in the two-level form described under Quote Type. On such a rate, rely on total_buy_rate and total_sell_rate, not on the individual lines.

Lines of an FCL or drayage rate with a markup​

When an FCL rate, or an origin or destination drayage rate, has a markup set in Starboard, the whole markup is billed on the freight line and every other billed line sells at its cost. The API sends the billed lines (is_included_in_total: true) the way Starboard bills them:

  • buy_rate and sell_rate are in the quote's display currency, and currency says so. Added up, they equal the rate's total_buy_rate and total_sell_rate.
  • The freight line's sell_rate includes the markup. A rate with several container types has a freight line per container type, each with its own container's markup.
  • unit_rate is the per-unit buy, in the display currency. On the freight line, user_rate_override is the per-unit sell. Other lines have user_rate_override only if a sell was entered for them, and it then equals unit_rate.
  • On a rate with several container types, a charge with no equipment_type is billed once per container type and sent as a single line, so its buy_rate need not equal unit_rate × quantity.
  • Lines the customer doesn't pay (is_included_in_total: false) are sent as stored, in their own currency.
  • If Starboard can't send the lines this way (for example, an exchange rate is missing, only some of the container types have a markup, or the rate's saved totals can't be reproduced from its lines), all of the rate's lines are sent as stored, in their own currencies, and may not add up to the totals.

CargoWise charge codes​

For companies with the CargoWise integration turned on, once Starboard has imported your CargoWise charge codes, each rate item carries cargowise_charge_code: the code the line is billed under in your own CargoWise. Use it to set each charge line's code instead of mapping name yourself.

  • It's on quotes with status pending_client_feedback, win, booking_requested, booking_confirmed or booking_rejected, and on a pending_review quote whose PDF or Excel quote has been generated. A quote that goes back to draft (shown as pending_review) loses it. Lost and expired quotes don't have it.
  • A line no specific code fits, or one Starboard hasn't matched to a code yet, carries its rate's freight code (the code of the leg's freight, for example air freight on an air rate). If the freight has no code either, it carries the code your company chose as its catch-all. With neither, the field is absent on that line.
  • It's also absent when Starboard couldn't work out the codes for that read: then no line in the response has it. Read the quote again later to get them.
  • The freight line carries it too, including the freight line the API adds to an air, LCL or cartage rate whose freight isn't saved as a line of its own.
  • The codes are worked out each time you read the quote, so a line's code can change, for example after a mapping is corrected in Starboard, without updated_at moving. See Detecting changed quotes.
  • A read takes longer when the quote has lines Starboard hasn't seen before, while it matches them: usually under a second, at most about 30 seconds. A List Quotes page waits once for all of its quotes, not once per quote. Later reads reuse the matches.

EquipmentType Type​

Represents a container type with quantity.

PropertyTypeDescription
typestringEquipment type (e.g., 40HC)
quantitynumberNumber of containers

Address Type​

Represents a physical address.

PropertyTypeDescription
streetstringStreet address
citystringCity
statestringState/Province
zipstringPostal code
countrystringCountry
latnumberLatitude
lngnumberLongitude

Operations​

List Quotes​

Retrieve a list of quotes with filtering options.

Without updated_since, quotes come back newest first by created_at, the time the quote was created, not the time its status or contents last changed. A quote created long ago and won today can therefore sit on a late page, so limit alone won't surface recently won or edited quotes. To find those, pass updated_since: only quotes changed at or after that time come back, oldest change first (updated_at ascending, then id). See Incremental sync with updated_since.

Endpoint

GET /api/v3/quotes

Query Parameters

ParameterTypeDescription
statusstringFilter by one status (see QuoteStatus). One value per request: a list isn't accepted. pending_review returns both draft and generated quotes; draft or generated returns just one of them. Without status, every status except temp is returned: temp quotes appear only with status=temp.
customer_company_idstringFilter by customer company ID. Must be a UUID.
quote_numberstringCase-insensitive partial match against the quote number shown on the quote (quote_number) and Starboard's internal quote number, so Q-1 also matches Q-10. Compare the numbers exactly on your side.
rfq_idstringFilter by RFQ ID. Must be a UUID.
updated_sincestringOnly quotes whose updated_at is at or after this time: an RFC 3339 timestamp, such as 2026-11-03T14:56:41.218Z (fractional seconds are optional). Quotes then come back oldest change first (updated_at ascending, then id), and the cursor follows that order: see Pagination below. Malformed values return 400. URL-encode it: a + in a time zone offset must be sent as %2B, or use Z.
pushedbooleantrue: only quotes whose last push recorded with Record Push Status has status success, and that have an external_quote_number. false: every other quote — no push recorded, the last push failed or in_progress, or no external_quote_number. Send true or false in lowercase: any other value returns 400. status=win&pushed=false lists the won quotes not yet imported into your system.
limitnumberNumber of results per page: default 20, maximum 50 (larger values are treated as 50). See Pagination.
cursorstringPagination cursor from the previous response. Send the same filters and limit with it.

Response

PropertyTypeDescription
dataQuote[]Array of quotes
totalnumberTotal number of matching quotes, counted again on every page
cursorstringCursor for the next page; "" on the last page

Error Responses

Status CodeDescription
400Bad Request - Invalid status value
400Bad Request - updated_since isn't an RFC 3339 timestamp (message: Invalid updated_since; expected RFC3339 timestamp (e.g. 2026-01-02T15:04:05Z))
400Bad Request - pushed is neither true nor false (message: Invalid pushed; expected true or false)
400Bad Request - With updated_since, a cursor that isn't valid for it: garbled, or taken from a list without updated_since (message: Invalid cursor). Start the walk again from your mark, without cursor. Without updated_since, an invalid cursor silently restarts from the first page instead.
401Unauthorized - Invalid credentials
500Internal Server Error. Also returned when customer_company_id or rfq_id isn't a UUID.

Every 400 comes back in the usual error body, with "error": "bad_request" (see Error Handling).

Example

Request:

curl 'https://<your-starboard-api-host>/api/v3/quotes?status=win&limit=10' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}'

Response:

{
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"company_id": "00000000-0000-0000-0000-000000000001",
"rfq_id": "00000000-0000-0000-0000-000000000002",
"quote_number": "Q-000001",
"status": "win",
"customer_company_id": "00000000-0000-0000-0000-000000000003",
"incoterm": "FOB",
"commodity": "Electronics",
"effective_date": "2025-01-01",
"expiring_date": "2025-01-31",
"display_currency": "USD",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z",
"lanes": [
{
"id": "00000000-0000-0000-0000-000000000004",
"primary_service_type": "ocean_fcl",
"sort_order": 0,
"legs": [ ... ],
"cargo": [ ... ],
"cargo_v2": { ... }
}
],
"routes": [
{
"id": "00000000-0000-0000-0000-000000000004",
"sort_order": 0,
"shipment_type": "FCL",
"equipment_type": "40HC",
"equipment_types": [{ "type": "40HC", "quantity": 1 }],
"quantity": 1,
"pickup_location": "123 Origin St, Shanghai, China",
"origin": "Shanghai, SH, China",
"destination": "Los Angeles, CA, USA",
"delivery_location": "456 Dest Ave, Los Angeles, CA",
"route_legs": [
{
"id": "00000000-0000-0000-0000-000000000005",
"service_type": "origin_drayage",
"sort_order": 0,
"origin": "Shanghai, SH, China",
"pickup_location": "123 Origin St, Shanghai, China",
"rates": null
},
{
"id": "00000000-0000-0000-0000-000000000006",
"service_type": "ocean_fcl",
"sort_order": 0,
"origin": "Shanghai, SH, China",
"destination": "Los Angeles, CA, USA",
"rates": null
},
{
"id": "00000000-0000-0000-0000-000000000007",
"service_type": "destination_drayage",
"sort_order": 0,
"destination": "Los Angeles, CA, USA",
"delivery_location": "456 Dest Ave, Los Angeles, CA",
"rates": null
}
]
}
],
"last_push_status": "failed",
"last_push_error": {
"code": "CW_4032",
"message": "invalid customer external_id",
"raw": { "attempt": 3 }
},
"last_push_integration": "cargowise_trm",
"last_push_at": "2026-02-14T09:33:12.481Z"
}
],
"total": 37,
"cursor": "eyJvIjoxMH0"
}

Pagination

To retrieve additional results, send the same filters and limit again with the cursor from the previous response (see Pagination):

curl 'https://<your-starboard-api-host>/api/v3/quotes?status=win&limit=10&cursor=eyJvIjoxMH0' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}'

With updated_since, the cursor is a position in updated_at order rather than a page number:

  • A quote that changes while you page moves to the end with its new updated_at, so the walk lists it again rather than skipping it. Quotes created while you page come at the end too. The one exception is a change that commits after the walk has passed its updated_at: the 2-minute overlap in Incremental sync with updated_since picks it up on your next poll.
  • Send the same filters, updated_since and limit with each cursor.
  • The cursor only pages within one walk: the last page returns "cursor": "". Your next poll is a new walk, with updated_since set to the highest updated_at you processed minus 2 minutes, and no cursor (see Incremental sync with updated_since).
  • An invalid cursor returns 400 instead of restarting from the first page.
  • total counts the quotes that match all your filters, updated_since and pushed included, again on every page.

Example: won quotes changed since your last poll

The highest updated_at you processed last time (your mark) was 2026-11-03T14:58:41.218Z. Ask for the won quotes changed since 2 minutes before it:

curl 'https://<your-starboard-api-host>/api/v3/quotes?status=win&updated_since=2026-11-03T14:56:41.218Z&limit=50' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}'

Response (each quote shortened):

{
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"quote_number": "Q-000001",
"status": "win",
"updated_at": "2026-11-03T14:58:41.218Z",
"lanes": [ ... ],
"routes": [ ... ]
},
{
"id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f",
"quote_number": "Q-000042",
"status": "win",
"updated_at": "2026-11-03T15:01:07.904Z",
"lanes": [ ... ],
"routes": [ ... ]
}
],
"total": 2,
"cursor": ""
}

Q-000001 comes back with the id and updated_at you already processed (that's the overlap): skip it. Process Q-000042, then save 2026-11-03T15:01:07.904Z as your new mark. The cursor is empty, so this walk is done; had it not been, you'd request the next page with the same parameters plus cursor. Your next poll starts a new walk, with updated_since=2026-11-03T14:59:07.904Z (the new mark minus 2 minutes) and no cursor.

Example: won quotes not yet imported

List the won quotes whose last push isn't a success, or that have no external_quote_number (at most once a minute, like any poll):

curl 'https://<your-starboard-api-host>/api/v3/quotes?status=win&pushed=false&limit=50' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}'

Response (each quote shortened), with one quote never pushed and one whose last push failed:

{
"data": [
{
"id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70",
"quote_number": "Q-000057",
"status": "win",
"updated_at": "2026-11-03T15:12:30.551Z",
"lanes": [ ... ],
"routes": [ ... ]
},
{
"id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7081",
"quote_number": "Q-000061",
"status": "win",
"updated_at": "2026-11-03T15:20:02.006Z",
"lanes": [ ... ],
"routes": [ ... ],
"last_push_status": "failed",
"last_push_error": {
"code": "INVALID_CUSTOMER",
"message": "invalid customer external_id"
},
"last_push_integration": "external_quote_sync",
"last_push_at": "2026-11-03T15:21:44.120Z"
}
],
"total": 2,
"cursor": ""
}

Import each quote into your system, then record the result with Record Push Status. Send the integration_type that's switched on for your company (external_quote_sync for your own system, cargowise_trm for CargoWise). A success with your quote number takes the quote off this list:

curl -X POST 'https://<your-starboard-api-host>/api/v3/quotes/d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f70/push_status' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}' \
-d '{
"integration_type": "external_quote_sync",
"status": "success",
"external_quote_number": "QBOS00012346"
}'
  • A failed import stays on the list. Record it with "status": "failed" and an error, then retry it later.
  • pushed follows the last push you recorded. A quote leaves the list once its last push is a success and it has an external_quote_number, sent on that call or an earlier one. A later failed or in_progress push puts it back on the list, even though the number stays recorded.
  • Edits after the import don't put a quote back on the list. A quote changed in Starboard after you imported it stays pushed=true. Use updated_since, or webhooks, to catch those changes.
  • Recording a push changes nothing else. It doesn't move updated_at, so it doesn't bring the quote back into your updated_since window or send a webhook event.

Read Quote​

Retrieve details of a specific quote, including the download URL if available.

Endpoint

GET /api/v3/quotes/{quote_id}

Path Parameters

ParameterTypeDescription
quote_idstringThe ID of the quote (UUID format)

Response

PropertyTypeDescription
quoteQuoteThe quote object
download_urlstringSigned download URL for the quote's PDF, valid for 1 hour. An empty string ("") when the quote has no PDF, or when the link couldn't be made; read the quote again to retry.

Error Responses

Status CodeDescription
400Bad Request - Invalid quote_id format
401Unauthorized - Invalid credentials
404Not Found - Quote doesn't exist
500Internal Server Error

Example

Request:

curl 'https://<your-starboard-api-host>/api/v3/quotes/a1b2c3d4-e5f6-7890-abcd-ef1234567890' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}'

Response:

{
"quote": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"company_id": "00000000-0000-0000-0000-000000000001",
"rfq_id": "00000000-0000-0000-0000-000000000002",
"quote_number": "Q-000001",
"status": "win",
"customer_company_id": "00000000-0000-0000-0000-000000000003",
"incoterm": "FOB",
"commodity": "Electronics",
"effective_date": "2025-01-01",
"expiring_date": "2025-01-31",
"display_currency": "USD",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z",
"lanes": [ ... ],
"routes": [
{
"id": "00000000-0000-0000-0000-000000000004",
"sort_order": 0,
"shipment_type": "FCL",
"equipment_type": "40HC",
"equipment_types": [{ "type": "40HC", "quantity": 1 }],
"quantity": 1,
"pickup_location": "123 Origin St, Shanghai, China",
"origin": "Shanghai, SH, China",
"destination": "Los Angeles, CA, USA",
"delivery_location": "456 Dest Ave, Los Angeles, CA",
"route_legs": [
{
"id": "00000000-0000-0000-0000-000000000005",
"service_type": "origin_drayage",
"sort_order": 0,
"origin": "Shanghai, SH, China",
"pickup_location": "123 Origin St, Shanghai, China",
"rates": null
},
{
"id": "00000000-0000-0000-0000-000000000006",
"service_type": "ocean_fcl",
"sort_order": 0,
"origin": "Shanghai, SH, China",
"destination": "Los Angeles, CA, USA",
"rates": null
},
{
"id": "00000000-0000-0000-0000-000000000007",
"service_type": "destination_drayage",
"sort_order": 0,
"destination": "Los Angeles, CA, USA",
"delivery_location": "456 Dest Ave, Los Angeles, CA",
"rates": null
}
]
}
],
"last_push_status": "success",
"last_push_integration": "cargowise_trm",
"last_push_at": "2026-07-08T22:10:31.207Z",
"external_quote_number": "QBOS00012345"
},
"download_url": "https://example.com/quote.pdf?token={signed-token}"
}

Download Quote Document​

Get the download URL for a quote document.

info

The document is available once the quote's PDF has been generated in Starboard, whatever the quote's status. A quote without a PDF returns 404.

Endpoint

GET /api/v3/quotes/{quote_id}/document-url

Path Parameters

ParameterTypeDescription
quote_idstringThe ID of the quote (UUID format)

Response

PropertyTypeDescription
quote_idstringThe quote ID
download_urlstringThe download URL for the quote document
warning

The returned download URL expires after 1 hour. Download the document right away rather than storing the URL.

Error Responses

Status CodeDescription
400Bad Request - Invalid quote_id format
401Unauthorized - Invalid credentials
404Not Found - The quote doesn't exist (message: Quote not found), or it has no PDF or its link couldn't be made (Quote document not found; if the quote's file_path is set, try again)
500Internal Server Error

Example

Request:

curl 'https://<your-starboard-api-host>/api/v3/quotes/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document-url' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}'

Response:

{
"quote_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"download_url": "https://example.com/quote.pdf?token={signed-token}"
}

Record Push Status​

Record the outcome of pushing a quote to an external system: CargoWise (through Switchboard), or your own system. The four last_push_* fields on the Quote type reflect the most recent call for that quote, and external_quote_number holds the number your system gave the quote, once you report one — subsequent reads via GET /api/v3/quotes/{quote_id} echo whatever this endpoint last stored. Calls to this endpoint do not change the quote's updated_at (see Detecting changed quotes), so they never send a webhook event. To list the won quotes whose last push isn't a success, or that have no external_quote_number, use List Quotes with status=win&pushed=false.

Each call names the integration reporting the push in integration_type, and that integration must be switched on for your company. There are two:

integration_typeUse it when
cargowise_trmYour quotes go into CargoWise through Switchboard.
external_quote_syncYour own system imports the quotes (an ERP, a TMS, a CRM, or anything else you run) and reports each push back. Your company doesn't need CargoWise for this.

Starboard switches the integration on for your company: ask your Starboard team for the one that fits. A call naming an integration that isn't switched on returns 403 integration_disabled.

Endpoint

POST /api/v3/quotes/{quote_id}/push_status

Path Parameters

ParameterTypeDescription
quote_idstringThe ID of the quote (UUID format). Must belong to your company.

Request Body

FieldTypeRequiredDescription
integration_typestringYesThe integration reporting the push: cargowise_trm (CargoWise, through Switchboard) or external_quote_sync (your own system). Must be switched on for your company; see above.
statusstringYesOne of success, failed, in_progress.
errorobjectConditionalRequired when status = "failed". Must be a JSON object (not a string or array). Forbidden when status is success or in_progress. Shape is caller-defined — typical fields are code, message, and an optional raw object with the vendor's original payload.
external_quote_numberstringNoThe quote's number in your system (for example, the quote number CargoWise assigned when you created the quote there). Surrounding whitespace is trimmed; the result must be 1–100 characters. Allowed with any status. When omitted, the stored value is left unchanged — a later call without it (for example, a failed re-push) never clears it. Sending null is treated the same as omitting it. A stored number can't be cleared through the API; send the corrected value instead.

Response

Echoes the persisted state so callers can confirm exactly what was stored. last_push_at is set by Starboard when the call is received.

PropertyTypeDescription
quote_idstringThe quote ID
last_push_statusstringThe recorded status (success, failed, or in_progress)
last_push_errorobjectThe recorded error blob (present only when status is failed)
last_push_integrationstringThe recorded integration tag
last_push_atstringISO 8601 timestamp of when this call was recorded
external_quote_numberstring or nullThe stored external quote number after this call: the value you just sent, the previously stored one if you omitted it, or null if none has ever been reported

Error Responses

Status CodeDescription
400Bad Request — invalid quote_id UUID; unparseable JSON body; missing integration_type; missing status; unknown status value; failed without error; error provided when status isn't failed; error isn't a JSON object; external_quote_number isn't a string, is blank, or is longer than 100 characters.
401Unauthorized — missing or invalid credentials
403integration_disabled — integration_type is not switched on for your company. Ask your Starboard team to switch it on.
404Not Found — quote doesn't exist for your company
500Internal Server Error

Example — recording a failure

Request:

curl -X POST 'https://<your-starboard-api-host>/api/v3/quotes/a1b2c3d4-e5f6-7890-abcd-ef1234567890/push_status' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}' \
-d '{
"integration_type": "cargowise_trm",
"status": "failed",
"error": {
"code": "CW_4032",
"message": "invalid customer external_id",
"raw": { "attempt": 3 }
}
}'

Response:

{
"quote_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"last_push_status": "failed",
"last_push_error": {
"code": "CW_4032",
"message": "invalid customer external_id",
"raw": { "attempt": 3 }
},
"last_push_integration": "cargowise_trm",
"last_push_at": "2026-07-08T22:04:04.813Z",
"external_quote_number": null
}

Example — recording a success with the external quote number (no error field)

curl -X POST 'https://<your-starboard-api-host>/api/v3/quotes/a1b2c3d4-e5f6-7890-abcd-ef1234567890/push_status' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}' \
-d '{
"integration_type": "cargowise_trm",
"status": "success",
"external_quote_number": "QBOS00012345"
}'

Response:

{
"quote_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"last_push_status": "success",
"last_push_integration": "cargowise_trm",
"last_push_at": "2026-07-08T22:10:31.207Z",
"external_quote_number": "QBOS00012345"
}

Example — recording an in-progress attempt

Use in_progress when the push has been initiated but hasn't yet concluded. Like success, no error field is allowed.

curl -X POST 'https://<your-starboard-api-host>/api/v3/quotes/a1b2c3d4-e5f6-7890-abcd-ef1234567890/push_status' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}' \
-d '{
"integration_type": "cargowise_trm",
"status": "in_progress"
}'
info

Each call overwrites the four last_push_* fields with the new outcome — the endpoint records the last known state, not a history. external_quote_number is different: it is only written when the request includes it, so you can send it once (typically with the first success) and omit it on later calls. If you need a full audit trail of every push attempt, keep it in your own system.

info

For companies with either integration switched on, Starboard shows the last push on the quote, named after the integration that recorded it: for cargowise_trm, the web app's card is titled CargoWise TRM and the email add-ins say CargoWise; for external_quote_sync, both say External system. The quote page in the web app shows its status, any external_quote_number, and the error for a failure. The quote header of the Starboard email add-ins (Outlook, Gmail and Front) shows only a failure and any external_quote_number. For a failure, both show error.code and error.message, joined by " — ", when they are strings, and otherwise the first 200 characters of the raw error JSON, so write message for a person to read.

Update Quote Customer​

Set which company in your Starboard CRM a quote belongs to, using that company's external_id (for example, its CargoWise organization code).

Endpoint

PUT /api/v3/quotes/{quote_id}/customer

Path Parameters

ParameterTypeDescription
quote_idstringThe ID of the quote (UUID format). Must belong to your company.

Request Body

FieldTypeRequiredDescription
external_idstringYesThe customer's code in your system. Surrounding whitespace is trimmed; the result must be 1–100 characters of letters, numbers, hyphens (-), underscores (_) and slashes (/), the same rule as a company's external_id. It's matched exactly, case-sensitive: exlog01 doesn't find a company whose external_id is EXLOG01.
organizationobjectNoYour own record of the organization behind external_id. Starboard reads it only when no company has external_id (see below) and never saves any of it. Fields, all optional strings: name (up to 300 characters), address (300), city (100), state (100), postal_code (40), country_code (a 2-letter ISO 3166 code, or empty) and email (320).

What happens

  1. A company in your CRM has external_id: it becomes the quote's customer (result: "switched"). If it already was, nothing is written ("unchanged"), so repeating a call is safe.

  2. No company has external_id: Starboard checks whether organization is the customer already on the quote. If it is, Starboard saves external_id on that customer ("linked"). The quote keeps its customer, and later calls with that external_id find the company directly. Starboard only links when all of these hold:

    • the quote has a customer, and that customer is an active company in your CRM with no external_id yet. Starboard never overwrites a stored external_id;
    • you sent organization.name. Send the address, country_code and email too: they make the check more reliable;
    • the CargoWise integration is turned on for your company;
    • Starboard's check finds that organization is that customer. Two different country_codes never match, and neither does another branch, office or subsidiary of the same company.

    In every other case the request is refused and nothing changes.

Switching the customer advances the quote's updated_at (see Detecting changed quotes). A link changes the company rather than the quote, so it advances the company's updated_at (see incremental sync). Prices are not recalculated either way.

Response

PropertyTypeDescription
quote_idstringThe quote ID
customer_company_idstringThe quote's customer after this call
previous_customer_company_idstring or nullThe quote's customer before this call; null if it had none
external_idstringThe external_id you sent, trimmed
resultstringswitched, unchanged or linked

Error Responses

Every refusal carries an error code and a message written for a person. Show the message to whoever looks after your customers: it says what to fix in the Starboard CRM.

Status CodeerrorDescription
400bad_requestInvalid quote_id UUID; unparseable JSON body; external_id missing, blank, longer than 100 characters or with other characters; an organization field too long; country_code not 2 letters.
401unauthorizedMissing or invalid credentials
404not_foundThe quote doesn't exist for your company
409external_id_archivedThe company with external_id is archived. Reactivate it, or move the code to the right company.
409customer_has_external_idNo company has external_id, and the quote's customer already has a different one. Correct it in the Starboard CRM. Also returned when the customer changed while the request ran (it was given another external_id or archived); retry to see why.
409external_id_takenAnother company was given external_id while the request ran. Retry: the quote then moves to that company.
422external_id_not_foundNo company has external_id, and it couldn't be linked to the quote's customer. The message says why: for example, the quote has no customer, organization.name is missing, or the CargoWise integration is off.
422customer_mismatchNo company has external_id, and organization doesn't look like the quote's customer.
503match_unavailableStarboard couldn't run the check. Nothing was changed; retry later.
500internal_server_errorInternal Server Error

Example: moving the quote to the company with external_id

curl -X PUT 'https://<your-starboard-api-host>/api/v3/quotes/a1b2c3d4-e5f6-7890-abcd-ef1234567890/customer' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}' \
-d '{ "external_id": "GL-2024-001" }'

Response:

{
"quote_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"customer_company_id": "00000000-0000-0000-0000-000000000003",
"previous_customer_company_id": "00000000-0000-0000-0000-000000000009",
"external_id": "GL-2024-001",
"result": "switched"
}

Example: linking a new code to the quote's customer

curl -X PUT 'https://<your-starboard-api-host>/api/v3/quotes/a1b2c3d4-e5f6-7890-abcd-ef1234567890/customer' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {company-id}' \
-d '{
"external_id": "EXLOGSHA",
"organization": {
"name": "EXAMPLE LOGISTICS (SHANGHAI) CO., LTD.",
"address": "88 Century Ave",
"city": "Shanghai",
"country_code": "CN",
"email": "ops@example.com"
}
}'

Response:

{
"quote_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"customer_company_id": "00000000-0000-0000-0000-000000000003",
"previous_customer_company_id": "00000000-0000-0000-0000-000000000003",
"external_id": "EXLOGSHA",
"result": "linked"
}

Example: a refusal

{
"error": "customer_has_external_id",
"message": "No company in your Starboard CRM has external_id EXLOGSHA, and this quote's customer is already linked to external_id EXLOG01. A stored code is never overwritten: correct it in your Starboard CRM, then retry."
}

Enums​

QuoteStatus Enum​

Possible status values returned in API responses:

ValueDescription
pending_reviewAwaiting internal review
pending_rateAwaiting carrier rates
pending_client_feedbackAwaiting client feedback
action_requiredAction needed
winAccepted
lossRejected
expiredExpired
booking_requestedBooking requested
booking_confirmedBooking confirmed
booking_rejectedBooking rejected
tempTemporary, incomplete quote. Left out of List Quotes unless you filter with status=temp.
note
  • draft and generated quotes are returned with status pending_review
  • For companies that book air freight through Starboard, a won quote can move on from win to booking_requested, then to booking_confirmed or booking_rejected (or back to pending_review if the booking fails). It then no longer matches status=win.

EquipmentType Enum​

ValueDescription
20GP20-ft GP container
20HC20-ft High Cube
40GP40-ft GP container
40HC40-ft High Cube
45GP45-ft GP (extra length)
45HC45-ft High Cube + length
40NOR40-ft insulated
40REEF40-ft refrigerated
20REEF20-ft refrigerated
40RH40-ft High Cube refrigerated
20OT20-ft open top
40OT40-ft open top
20FR20-ft flat rack
40FR40-ft flat rack

The list can grow: accept values you don't recognize rather than failing on them.

ShipmentType Enum​

ValueDescription
FCLFCL (Full Container Load)
LCLLCL (Less than Container Load)
AIRAir Freight

On a route whose lane has only trucking, shipment_type is the lane's service type as is: origin_drayage, destination_drayage, origin_cartage or destination_cartage (see ServiceType).

ServiceType Enum​

ValueDescription
ocean_fclOcean FCL
ocean_lclOcean LCL
airAir Freight
origin_drayageDrayage to origin port
destination_drayageDrayage from destination port
origin_cartageCartage to origin (LCL/Air)
destination_cartageCartage from destination (LCL/Air)