QuotesPBC Web Components - Customer Integration Guide
Which widget do you need?
This guide covers more than one component. Pick by what you are building:
| Widget | Use it when | Section |
|---|---|---|
<ready-zipcode-quote> |
You want a price for a zip-to-zip move, nothing more | Quick Start below |
<ready-contact-quote> |
You want a quote plus an editable delivery address | Quick Start below |
<quote-order-component> |
You want the full quote → review → place-order flow (multiple transport options, high-value handling, order submission) | Quote/Order Widget at the end |
The first two are documented immediately below and remain supported. The Quote/Order widget is the newest and is the only one that can place an order — if you need ordering, skip to its section.
Everything in "Styling & Customization", "Security & Authentication" and "Browser Support" applies to all three.
🚀 Quick Start
Integrate transportation quotes into your website with just one line of code! Our web components are self-contained and require no additional dependencies.
Simple Integration
Add this single script tag to your HTML page — one tag, nothing else:
<script src="https://dev-logistics-pbc.awsmanlog10np.manheim.com/v/1.3.0/quotespbc-ui-loader.js"></script>
Environments
| Environment | Script URL |
|---|---|
| Dev | https://dev-logistics-pbc.awsmanlog10np.manheim.com/v/1.3.0/quotespbc-ui-loader.js |
| Stage | https://stage-logistics-pbc.awsmanlog10np.manheim.com/v/1.3.0/quotespbc-ui-loader.js |
| Prod | https://prod-logistics-pbc.awsmanlog10.manheim.com/v/1.3.0/quotespbc-ui-loader.js |
Choosing a loader URL
Every release is published in its own folder, and each URL above pins this guide's version, 1.3.0. Two shorter paths follow newer releases for you:
| Path | Loads | Use it when |
|---|---|---|
/v/1.3.0/ |
Exactly 1.3.0, never anything else | Production. Upgrade deliberately, after testing the new version. |
/v/M.m/ (the first two numbers of the version) |
The newest patch release of that minor line | You want fixes as soon as they are released, and nothing else. |
/v/M/ (the first number of the version) |
The newest minor or patch release of that major line | Non-production pages that should track new features. A major version is never switched for you. |
Each path keeps working for as long as its version is supported. The version list at the root of the script host shows every published version and its support status.
And the API host the widgets call, api-config.bffUrl:
| Environment | BFF URL (api-config.bffUrl) |
|---|---|
| Dev | https://dev-logistics-pbc-bff.awsmanlog10np.manheim.com |
| Stage | https://stage-logistics-pbc-bff.awsmanlog10np.manheim.com |
| Prod | https://prod-logistics-pbc-bff.awsmanlog10.manheim.com |
These are two different hosts, deliberately. The script origin serves the widget; the BFF
URL is the API the widget calls. Note the -bff in the host name, and that prod alone drops
the np (non-production) segment. Give no path or trailing slash — the widget appends its
own routes.
Point your non-production environments at dev or stage, never at prod.
What the loader does. It works out its own base URL from its own <script src>, then injects
the module and nomodule entry scripts for you, each stamped with a build-time cache-busting
version. That is why you get a single tag and never hard-code a /build/… path: when we
release, the version changes and browsers pick up the new bundle without you doing anything.
There is no separate CDN — the widget is served by its own deployment, so the host above is both the script origin and the app origin.
Then use the components anywhere on your page:
<!-- Zip-to-zip quote -->
<ready-zipcode-quote
api-config='{"bffUrl": "https://dev-logistics-pbc-bff.awsmanlog10np.manheim.com", "token": "bearer eyJ..."}'
quote-request='{ "partnerId": "YOUR_PARTNER_ID", "vehicles": [ … ], "stops": [ … ] }'></ready-zipcode-quote>
<!-- Quote with an editable delivery address -->
<ready-contact-quote
api-config='{"bffUrl": "https://dev-logistics-pbc-bff.awsmanlog10np.manheim.com", "token": "bearer eyJ..."}'
quote-request='{ "partnerId": "YOUR_PARTNER_ID", "vehicles": [ … ], "stops": [ … ] }'></ready-contact-quote>
<!-- Full quote -> review -> place-order flow (newest; see its own section below) -->
<quote-order-component
api-config='{"bffUrl": "https://dev-logistics-pbc-bff.awsmanlog10np.manheim.com", "token": "bearer eyJ..."}'
quote-request='{ "partnerId": "YOUR_PARTNER_ID", "vehicles": [ … ], "stops": [ … ] }'
order-source="OVE"
customer-id="5000001"></quote-order-component>
All three widgets take the same two objects:
api-config(where the API is, and your token) andquote-request(the move: vehicles and stops). Your partner ID is part of the move,quote-request.partnerId. The Current API Reference below has their full shape.<quote-order-component>also takesorder-source,customer-idand the rest; see its own section for the full attribute list.Pass each object as a JSON attribute, as above, or set it as a DOM property from JavaScript:
const el = document.getElementById('quote'); el.apiConfig = { bffUrl: 'https://dev-logistics-pbc-bff.awsmanlog10np.manheim.com', token: 'bearer eyJ...' }; el.quoteRequest = { /* vehicle + stops — same shape as below */ }; el.orderSource = 'OVE'; el.customerId = '5000001';
📋 Current API Reference (Object-Based)
Component Properties Overview
All three components take object properties, passed either as JSON attributes or as DOM properties set from JavaScript:
ready-contact-quoteandready-zipcode-quote— documented in this sectionquote-order-component— sharesapi-configandquote-requestexactly as described below, and addsorder-source,customer-id,contactanduser-email. See its own section for those.
Common Properties
api-config (required) - API configuration object:
{
bffUrl: string, // Backend for Frontend API URL
token: string // Bearer authentication token
}
quote-request (required) - Quote request object:
{
itemRef: string,
partnerId: string,
payerId: string,
shipperId: string,
quoteType: string, // e.g., "Standard", "Scheduled"
quoteSubType: string, // e.g., "NewToConsumer", "TradeIn"
vehicles: [{
vin: string,
pickUpId: string, // References stop refId
dropOffId: string, // References stop refId
isOperable: boolean, // REQUIRED — send true or false explicitly, never omit (see below)
isEnclosed: boolean,
description: {
year: string,
make: string,
model: string,
subModel: string
}
}],
stops: [{
refId: string, // e.g., "s1", "s2"
zip: string,
address1: string,
address2: string,
city: string,
state: string,
country: string // e.g., "US"
}]
}
⚠️
isOperablemust be sent explicitly —trueorfalse. Do not omit it.It is a boolean, so an absent value is
false: a vehicle with noisOperableis treated as inoperable for both pricing and ordering. Inoperable vehicles need winching and special handling, so they generally price higher — omitting the field can quote your customer a price for a condition they never declared.This applies consistently across
POST /Quote,POST /Quote/multiandPOST /Order(US2143547). Before that change the order path treated an absent value as operable while the quote path treated it as inoperable, so an omitting host was priced for one condition and ordered another.
ready-contact-quote Specific
quote-id (optional) - Existing quote ID for retrieval:
quoteId: string // UUID format
quote-contacthas been removed. The widget no longer shows or edits contact details (name, phone, email), and no longer publishesContactUpdated. Stop passingquote-contact; if a page still sets it, it has no effect and causes no error.
The first quote. Supply a ZIP code on both the pickup and the delivery stop. The ZIP code is what the move is priced on; for the delivery stop it is also where City and State come from — the widget does not send your delivery City and State to the quote service, and replaces them on screen with the ones returned for the ZIP code (so a wrong City or State you supply is corrected, not rejected).
| What you supply | What the customer sees | What you receive on quote_bus |
|---|---|---|
| Valid pickup and delivery ZIP codes, and the quote succeeds | The price, with the delivery City and State from the quote | QuoteCreated |
| A pickup or delivery ZIP code that is missing or malformed (US: 5 digits or ZIP+4; a missing country counts as US) | "Unable to get transportation quote at this time. Please try again later." in place of the price | Nothing — no quote is requested, so check your own input first |
| Valid ZIP codes, but the quote fails (the ZIP cannot be served, the service is down, a timeout) | The same message in place of the price | QuoteError with the code of the failure |
Editing the delivery address. The customer can edit the delivery address from the Transport page. The ZIP code and Address Line 1 are required; City and State are not validated, because the ZIP code decides the price — the widget replaces City and State with the ones the quote service returns for that ZIP code. An edit is saved only when the new quote succeeds:
| Outcome of the new quote | What the customer sees | What you receive on quote_bus |
|---|---|---|
| Success | The new address and price | QuoteCreated with the new quoteId — use it for ordering |
| The ZIP code cannot be served | The edit page stays open, with the error under the ZIP code | QuoteError with code quote.delivery_zip_unserviceable or quote.delivery_zip_not_routable. The previous quote is still valid |
| Service down, timeout or any other error | Back on the Transport page with the previous address, and "Unable to get transportation quote at this time. Please try again later." in place of the price | QuoteError with code quote.http_<status> (e.g. quote.http_500) or quote.request_failed. Do not place an order with the previous quote — the customer's address change was not applied |
| Customer cancels (even while the quote is loading) | The previous address and price | Nothing |
Usage Example (Current API)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Transportation Quotes</title>
<script src="https://dev-logistics-pbc.awsmanlog10np.manheim.com/v/1.3.0/quotespbc-ui-loader.js"></script>
</head>
<body>
<h1>Get Your Quote</h1>
<ready-contact-quote id="quote-component"></ready-contact-quote>
<script>
// Wait for component to be ready
document.addEventListener('DOMContentLoaded', () => {
const component = document.getElementById('quote-component');
// Set API configuration
component.apiConfig = {
bffUrl: 'https://dev-logistics-pbc-bff.awsmanlog10np.manheim.com',
token: 'bearer <your-token>'
};
// Set quote request
component.quoteRequest = {
itemRef: 'ref-123',
partnerId: 'YOUR_PARTNER_ID',
payerId: 'payer-123',
shipperId: 'shipper-123',
quoteType: 'Standard',
quoteSubType: 'NewToConsumer',
vehicles: [{
vin: '1HGBH41JXMN109186',
pickUpId: 's1',
dropOffId: 's2',
isOperable: true,
isEnclosed: false,
description: {
year: '2020',
make: 'Honda',
model: 'Accord',
subModel: 'EX'
}
}],
stops: [
{
refId: 's1',
zip: '90210',
address1: '',
address2: '',
city: 'Beverly Hills',
state: 'CA',
country: 'US'
},
{
refId: 's2',
zip: '10001',
address1: '',
address2: '',
city: 'New York',
state: 'NY',
country: 'US'
}
]
};
});
</script>
</body>
</html>
React Example (Current API)
import { useEffect, useRef } from 'react';
function QuotePage() {
const componentRef = useRef(null);
useEffect(() => {
// Load the script
const script = document.createElement('script');
script.src = 'https://dev-logistics-pbc.awsmanlog10np.manheim.com/v/1.3.0/quotespbc-ui-loader.js';
script.onload = () => {
// Configure component after script loads
if (componentRef.current) {
componentRef.current.apiConfig = {
bffUrl: 'https://dev-logistics-pbc-bff.awsmanlog10np.manheim.com',
token: 'bearer YOUR_TOKEN'
};
componentRef.current.quoteRequest = {
itemRef: 'ref-123',
partnerId: 'YOUR_PARTNER_ID',
payerId: 'payer-123',
shipperId: 'shipper-123',
quoteType: 'Standard',
quoteSubType: 'NewToConsumer',
vehicles: [{
vin: '1HGBH41JXMN109186',
pickUpId: 's1',
dropOffId: 's2',
isOperable: true,
isEnclosed: false,
description: {
year: '2020',
make: 'Honda',
model: 'Accord',
subModel: 'EX'
}
}],
stops: [
{ refId: 's1', zip: '90210', address1: '', address2: '',
city: 'Beverly Hills', state: 'CA', country: 'US' },
{ refId: 's2', zip: '10001', address1: '', address2: '',
city: 'New York', state: 'NY', country: 'US' }
]
};
}
};
document.head.appendChild(script);
return () => {
if (script.parentNode) {
document.head.removeChild(script);
}
};
}, []);
return (
<div>
<h1>Get a Quote</h1>
<ready-contact-quote ref={componentRef}></ready-contact-quote>
</div>
);
}
export default QuotePage;
🎨 Styling & Customization
Built-in Design System
Our components come with Interstate Design System built-in, providing:
- ✅ Professional automotive industry styling
- ✅ Responsive design for all devices
- ✅ Accessibility compliance
- ✅ Consistent branding
Custom CSS (Optional)
You can customize colors and spacing with CSS custom properties:
/* Override component colors */
ready-contact-quote {
--primary-color: #your-brand-color;
--accent-color: #your-accent-color;
--text-color: #333333;
--background-color: #ffffff;
}
/* Adjust component width */
ready-contact-quote,
ready-zipcode-quote {
max-width: 400px;
margin: 0 auto;
}
/* quote-order-component is designed for 320-560px */
quote-order-component {
max-width: 560px;
margin: 0 auto;
}
Colour custom properties do not apply to
<quote-order-component>. It does not expose--primary-colorand friends — its interior is fully encapsulated, so sizing the element is the only styling hook. Raise visual changes with the team rather than trying to reach inside.
📱 Responsive Design
Components automatically adapt to different screen sizes:
- Desktop: Full-width layout with side-by-side inputs
- Tablet: Optimized for touch interaction
- Mobile: Stacked layout for easy thumb navigation
<quote-order-component> responds to its CONTAINER, not the viewport. It uses CSS
container queries and is designed for 320px–560px. That means it adapts correctly inside
a narrow sidebar on a wide desktop — but it also means giving it an unbounded container will
not look right. Put it in a wrapper with a max-width.
<ready-zipcode-quote> also responds to its own width, not the viewport. Below 800px it
stacks the quote under the ZIP entry; from 800px it collapses to a single short strip with the
quote beside it. Move that switch with wide-breakpoint (CSS pixels, no unit):
<ready-zipcode-quote wide-breakpoint="1000"></ready-zipcode-quote>
⚠️ Some
wide-breakpointvalues give a layout the widget was not designed for. It accepts any number, but its design was built and measured around the 800px default:
- Below roughly 780px. The wide strip's type is sized to fit on one line from about 780px. Below that, the delivery estimate and disclaimer wrap, the strip grows taller, and the vertical divider ends up at the start of the wrapped line. The lower the value, the narrower the containers that get the side-by-side layout, and the further the text wraps.
- Above 800px. The stacked card is capped at 310px wide, so any container narrower than the breakpoint but wider than 310px shows that narrow card with empty space beside it. A larger value widens that band — nothing breaks, but it may not look intended.
- Units other than
px. The value is read as a plain number of CSS pixels.wide-breakpoint="800px"works, but50remor80%are read as 50 and 80 pixels, which puts almost every container in the wide layout.- Not a number. Falls back to 800.
Change it only when your layout needs it, and check the widget on your own page at container widths either side of the new value before you ship.
🔒 Security & Authentication
API Token
Your token should be a valid Bearer token. The widget sends it exactly as you give it, as
the request's Authorization header, so include the bearer prefix:
bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
On all three widgets the token goes inside the api-config object:
el.apiConfig = { bffUrl: 'https://dev-logistics-pbc-bff.awsmanlog10np.manheim.com', token: 'bearer eyJ...' };
An invalid or expired token does not throw — the widget renders its unavailable state and
publishes pbc:error on quote_bus. If the widget "doesn't load", check that event first.
📊 Event Handling
Listen for Quote Events
Our components use BroadcastChannel to communicate events. Listen to the quote_bus
channel to receive quote events. All three components publish to the same channel.
<quote-order-component>publishes six event types the switch statement below does not cover —pbc:ready,pbc:error,MultiQuoteCreated,OrderSubmitted,OrderConfirmedandOrderFailed. It sharesQuoteError, and it does not publishQuoteCreated,QuoteUpdatedorContactUpdatedat all.If you are integrating that widget, read its event section rather than adapting the example below — there are three behaviours documented there that will bite a handler written against this one.
// Create a connection to the quote_bus channel
const channel = new BroadcastChannel('quote_bus');
// Listen for all quote events
channel.onmessage = function (event) {
console.log('Quote event received:', event.data);
// Handle different event types
switch (event.data.eventType) {
case 'QuoteCreated':
console.log('Quote created successfully:', event.data.eventBody);
// Handle successful quote creation (e.g., redirect, show confirmation)
break;
case 'QuoteError':
console.log('Quote error occurred:', event.data.eventBody);
// Handle error (e.g., show user-friendly message)
const errors = event.data.eventBody.errors;
errors.forEach(error => {
console.error(`Error ${error.code}: ${error.message}`);
// Display user-friendly error message
showErrorToUser(error.message);
});
break;
case 'QuoteUpdated':
console.log('Quote updated:', event.data.eventBody);
// Handle quote data changes
break;
// No component publishes ContactUpdated any more — ready-contact-quote stopped when
// contact details were removed from it.
default:
console.log('Unknown event type:', event.data.eventType);
}
};
// Alternative: Listen for specific event types only
channel.addEventListener('message', function (event) {
if (event.data.eventType === 'QuoteCreated') {
// Handle only quote creation events
handleQuoteCreated(event.data.eventBody);
}
});
// Don't forget to close the channel when done (optional)
// channel.close();
Example Event Payload
Quote Created Event:
### Example Event Payload
// Example of event.data when a quote is successfully created
{
"eventType": "QuoteCreated",
"eventBody": {
"currency": "USD",
"discount": 0,
"discountCurrency": "USD",
"distance": 11,
"distanceLabel": "MI",
"expirationDate": "2025-09-22T18:31:21Z",
"maxDaysToDeliver": 3,
"minDaysToDeliver": 2,
"price": 93,
"quoteId": "f2d1cce1-e1fe-4105-8070-7c6617f6dc11",
"quoteRef": "202509001623111",
"quoteSubType": "",
"quoteType": "Standard",
"stops": [
{
"distance": 0,
"address1": "",
"address2": "",
"city": "Phoenix",
"country": "US",
"refId": "S1",
"state": "AZ",
"zip": "85044"
},
{
"distance": 11,
"distanceLabel": "MI",
"address1": "",
"address2": "",
"city": "Phoenix",
"country": "US",
"refId": "S2",
"state": "AZ",
"zip": "85040"
}
],
"vehicles": [
{
"currency": "USD",
"description": "1991/Acura/Legend/L",
"descriptionDetails": {
"make": "Acura",
"model": "Legend",
"subModel": "L",
"year": "1991"
},
"discount": 0,
"discountCurrency": "USD",
"predictedBasePrice": 0,
"price": 93,
"priceLineItems": [
{
"currency": "USD",
"description": "FixedRate",
"price": 93
}
],
"vehicleSize": "S",
"weight": "3300",
"weightUnit": "pounds",
"dropOffId": "S2",
"isEnclosed": false,
"isOperable": true,
"pickUpId": "S1",
"vin": "JH4KA7561MC010714"
}
]
}
}
Quote Error Event:
// Example of event.data when an error occurs during quote processing
{
"eventType": "QuoteError",
"eventBody": {
"errors": [
{
"code": "quotes_api.bad_request",
"message": "An error occurred while calculating distance: Route at index 0: Stop at index 1: No matching postal code found Problem: [N/A] 11111 , ErrorId:fc26f1b3-5226-43b2-8837-269cc14b8c2d"
}
]
}
}
🐛 Troubleshooting
Common Issues
1. Component not appearing:
- ✅ Check that the script tag is loaded
- ✅ Verify the script URL is accessible
- ✅ Ensure no JavaScript errors in browser console
2. API errors:
- ✅ Verify that
api-config.tokenis valid, not expired, and starts withbearer - ✅ Check that
api-config.bffUrlis the BFF host for your environment - ✅ Confirm that
quote-request.partnerIdis registered - ✅ Listen for
pbc:erroronquote_bus:<quote-order-component>publishes it when it rejects its configuration.
3. Styling issues:
- ✅ Components use Shadow DOM for style isolation
- ✅ Use CSS custom properties for customization
- ✅ Check for conflicting CSS frameworks
Debug Mode
Enable debug logging by adding to your page:
window.QuotesPBCDebug = true;
Browser Support
Supported Browsers:
- ✅ Chrome 60+
- ✅ Firefox 55+
- ✅ Safari 12+
- ✅ Edge 79+
- ✅ Mobile browsers (iOS Safari, Chrome Mobile)
Legacy Support: Automatic fallback for older browsers using nomodule polyfills.
📞 Support
Quick Help
If you need immediate assistance:
- Check the browser console for error messages
- Verify all required properties are provided
- Test with this version's example page, published beside the loader at
/v/1.3.0/example-usage.html - Contact support with specific error details
🚀 Next Steps
- Get Your Credentials: Contact our team for your API token and partner ID — plus
orderSourceandcustomerIdif you are using<quote-order-component>, which needs both to place an order. That widget also accepts an optionaluserEmail: the address of the person using your site, which you supply yourself rather than request from us — see Identifying who accepted a warning - Test Integration: Use our sandbox environment for testing
- Go Live: Deploy to your production website
- Monitor: Use our analytics dashboard to track quote performance
Ready to get started? Contact us at integration@your-company.com
🚚 Quote/Order Widget (quote-order-component)
The newest widget, and the only one that places an order. It renders the whole flow: transport options → quote review → order confirmation.
The script and BFF URLs below are real, and the examples point at dev. Only the bearer token is still a placeholder — that one is per-partner and comes from the Transportation Management team. Everything else is taken from the shipped component contract.
What it does for you
- Offers the available transport options with prices, and hides ones that do not apply
- Handles high-value vehicles: applies the enclosed rule, and requires an acknowledgement if the shipper picks open transport anyway
- Never offers Expedited for an oversize vehicle
- Collects and validates contact phone and email, gating the Review Quote button
- Records, on the order itself, any warning the shipper accepted before submitting — see Identifying who accepted a warning
- Submits the order and reports the outcome
You supply the move and the credentials. These rules stay on our side, so they can change without you shipping anything.
Step 1 — load the script
There is no npm package and no CDN; the widget is served by its own deployment. Use the loader — one tag, and it resolves the bundle paths and cache-busting itself:
<script src="https://dev-logistics-pbc.awsmanlog10np.manheim.com/v/1.3.0/quotespbc-ui-loader.js"></script>
| Environment | Script URL |
|---|---|
| Dev | https://dev-logistics-pbc.awsmanlog10np.manheim.com/v/1.3.0/quotespbc-ui-loader.js |
| Stage | https://stage-logistics-pbc.awsmanlog10np.manheim.com/v/1.3.0/quotespbc-ui-loader.js |
| Prod | https://prod-logistics-pbc.awsmanlog10.manheim.com/v/1.3.0/quotespbc-ui-loader.js |
And the API host it calls, api-config.bffUrl:
| Environment | BFF URL (api-config.bffUrl) |
|---|---|
| Dev | https://dev-logistics-pbc-bff.awsmanlog10np.manheim.com |
| Stage | https://stage-logistics-pbc-bff.awsmanlog10np.manheim.com |
| Prod | https://prod-logistics-pbc-bff.awsmanlog10.manheim.com |
These are two different hosts, deliberately. The script origin serves the widget; the BFF
URL is the API the widget calls. Note the -bff in the host name, and that prod alone drops
the np (non-production) segment. Give no path or trailing slash — the widget appends its
own routes.
Loading the entry scripts directly instead (not recommended)
<script type="module" src="https://dev-logistics-pbc.awsmanlog10np.manheim.com/v/1.3.0/build/quotespbc-ui.esm.js"></script>
<script nomodule src="https://dev-logistics-pbc.awsmanlog10np.manheim.com/v/1.3.0/build/quotespbc-ui.js"></script>
This works, but you take on two things the loader handles for you: the /build/… paths become
part of your page, and you get no cache-busting parameter — so a browser can hold an older
bundle after we release.
Step 2 — place the element
Eight attributes: four objects, three strings, and one presence flag.
| Attribute | Property | Required | Purpose |
|---|---|---|---|
api-config |
apiConfig |
Yes | { bffUrl, token } |
quote-request |
quoteRequest |
Yes | The move: vehicle(s) + stops |
order-source |
orderSource |
Yes to order | Your channel identifier, e.g. OVE |
customer-id |
customerId |
Yes to order | Your customer identifier |
user-email |
userEmail |
No | Email of the person using your site |
contact |
contact |
Conditional | Contact used for the order |
quote-contact |
quoteContact |
No | Alternate contact input |
contact-required |
contactRequired |
No | Makes all three contact fields mandatory |
api-config and quote-request have the same shape as the other widgets — see
Current API Reference above. order-source,
customer-id, contact and user-email are new to this widget.
Two rows need a word more than the table gives them:
user-email— optional. It names whoever accepted a warning, and omitting it changes nothing except that the record names no one: Identifying who accepted a warningcontact— "Conditional" means you never have to supply it, but settingcontact-requiredmakes name, phone and email mandatory before the order can be placed, whether they come from you or from the shipper: Deciding whether contact is mandatory
Objects can be passed either as JSON string attributes or as DOM properties.
<quote-order-component id="quote"></quote-order-component>
<script type="module">
const el = document.getElementById('quote');
el.apiConfig = { bffUrl: 'https://dev-logistics-pbc-bff.awsmanlog10np.manheim.com', token: 'bearer eyJ...' };
el.orderSource = 'OVE';
el.customerId = '5000001';
el.userEmail = 'shipper@example.com';
el.quoteRequest = {
itemRef: '', partnerId: 'OVE', payerId: '', shipperId: '',
vehicles: [{
vin: 'JH4KA7561MC010714',
isOperable: true,
isEnclosed: false,
pickUpId: 'S1',
dropOffId: 'S2',
description: { year: '', make: '', model: '', subModel: '' },
// sizeOverride: 'OS' // "OS" = oversize; suppresses Expedited
}],
stops: [
{ refId: 'S1', zip: '85044', address1: '', address2: '', city: '', state: '', country: 'US' },
{ refId: 'S2', zip: '85040', address1: '4615 E Elwood St', address2: '#400',
city: 'Phoenix', state: 'AZ', country: 'US' },
],
};
el.contact = { name: 'Jane Roe', phone: '480-558-3200', email: 'jane@example.com' };
</script>
Minimum to render: a token, a bffUrl, at least two stops and at least one
vehicle. Anything less and the widget shows its unavailable state.
Stops match by refId, not position — pickUpId and dropOffId must each name a
refId present in stops.
Vehicle size — the one field that changes what is offered
sizeOverride accepts "S" | "M" | "L" | "XL" | "OS". OS means oversize, and
Expedited is then never offered. Any other value — or omitting the field — is treated as
not oversized. An unrecognised value is tolerated rather than rejected, so a typo like
"os" would silently offer Expedited.
High value is not something you declare
It is determined upstream and returned on the quote response as isHighValueVehicle. The
widget applies the enclosed rule and the open-transport acknowledgement on its own. You do
not send a flag for it.
Contact
contact: { name?: string, phone: string, email: string }
quoteContact: { name: string, phone: string, email: string }
Both carry a contact; contact wins field by field where both supply a value.
Contact is optional for you to supply:
- Supply it → fields are pre-filled, and the shipper can still edit them
- Omit it → the shipper enters it
- Supply something malformed → it is shown and flagged immediately, and must be fixed to continue. You do not have to wait for the shipper to touch the field
- Shipper edits always win. A late update from you fills only fields the shipper has not changed. Merely focusing a field, or typing and reverting, does not count as a change
Deciding whether contact is mandatory — contact-required
You choose, per embed:
<!-- contact name, phone and email are ALL mandatory -->
<quote-order-component contact-required></quote-order-component>
<!-- omit the attribute: all three are optional -->
<quote-order-component></quote-order-component>
It is presence-based, like required on an <input> — the bare attribute means yes,
and leaving it off means no. ="true" and ="false" also work if you are setting it
programmatically.
contact-required set |
attribute absent | |
|---|---|---|
| Contact name | mandatory | optional |
| Phone | mandatory | optional |
| mandatory | optional | |
| Review Quote waits for them | yes | no |
| A value you do supply is format-checked | yes | yes |
That last row is the important one: optional relaxes presence, never format. An empty phone is fine when the attribute is absent; a malformed one never is, in either mode.
If you intend contact to be mandatory, set the attribute. We cannot distinguish "the host wants optional" from "the host forgot", so a missing attribute is read as optional.
Phone format: send it however you hold it. 480-558-3200, 4805583200,
(480) 558-3200 and +1 480-558-3200 are all accepted and display identically as
480-558-3200. A leading 1 is treated as the country code only on an 11-digit value.
We never silently correct a bad number. If you send something that cannot be a NANP
number — say eleven digits that do not start with 1 — it is shown to the shipper exactly
as it arrived, unformatted, with "Invalid US phone number" beside it. We do not truncate it
into something that looks valid.
Phone validation is NANP-structural: ten digits, area code and exchange each starting
2–9, neither being an N11 code (911, 411…). An optional leading 1 is accepted. Note
555-123-4567 fails — the exchange 123 starts with 1.
Contact is never sent on a quote request — only on the order.
Identifying who accepted a warning — user-email
Two points in the flow ask the shipper to accept something before they can continue:
- picking open transport for a high-value vehicle, which requires a tick-box
- changing the vehicle condition away from the operable/inoperable value you supplied
When either happens, we record it on the order so dispatch and the carrier can see what was
agreed. user-email is how that record names a person:
<quote-order-component user-email="shipper@example.com"></quote-order-component>
el.userEmail = 'shipper@example.com';
Send the email of whoever is operating your site — the person clicking the tick-box. That may not be your customer of record, so do not simply pass the account holder's address if someone else is placing the order on their behalf.
It is optional, and omitting it is completely benign. The widget loads, quotes and places orders exactly as it does without it, and the record is still written — it simply names no one:
High Value Vehicle Open Transport Override accepted at 2026-09-16T14:31:48Z.
Note that variant reads passively, because with no user there is no subject to lead with.
It is the one case with a different word order, so do not write anything that assumes
<email> accepted <type> at <time> always holds.
We would still encourage you to send it. A record nobody can be tied to is of limited use in a dispute, which is most of why the record is kept. But that is advice, not a requirement: nothing is gated on it, nothing validates it, and no error is produced if you leave it out.
Supply it whether or not the shipper will hit a warning. You cannot know in advance: whether a vehicle is high-value is decided upstream and returned on the quote, and a condition change is the shipper's to make.
We do not validate or verify it. A malformed address is recorded exactly as you sent it, and we have no way to confirm the value belongs to the person at the keyboard — it is your assertion, and it is recorded as such. It is not derived from your bearer token, which identifies your application rather than a user.
Where the value ends up:
- In the order record, as a note visible to dispatch and carrier staff for the life of that order. Not customer-facing, but not internal to us either
- On the
OrderSubmittedandOrderFailedevents onquote_bus— see the privacy note in Step 3 below
If neither warning was accepted, nothing is recorded and the value is unused.
Step 3 — listen on quote_bus
Same BroadcastChannel as the other widgets (see Event Handling above), with additional event types for the order flow.
const channel = new BroadcastChannel('quote_bus');
channel.onmessage = ({ data }) => {
switch (data.eventType) {
case 'pbc:ready': /* widget mounted and configured */ break;
case 'pbc:error': /* { reason: 'unavailable' } */ break;
case 'MultiQuoteCreated': /* MultiQuoteResponse */ break;
case 'QuoteError': /* { errors: [{ code, message }] } — why quoting failed */ break;
case 'OrderSubmitted': /* { order } — fires per ATTEMPT */ break;
case 'OrderConfirmed': /* OrderResponse — { orderId?, confirmationNumber?, status, quoteId?, itemRef?, shipperId?, vehicle? } */ break;
case 'OrderFailed': /* { status, error, orderId?, confirmationNumber?, quoteId?, itemRef?, shipperId?, vehicle?, order } — may arrive with NO OrderSubmitted */ break;
}
};
eventType |
eventBody |
When |
|---|---|---|
pbc:ready |
(none) | Loaded and configured. Also fires again after each re-quote |
pbc:error |
{ reason: 'unavailable' } |
Config/token rejected, or no option could be priced |
MultiQuoteCreated |
MultiQuoteResponse |
Quotes returned |
QuoteError |
{ errors: [{ code, message }] } |
Quote request failed — carries the reason |
OrderSubmitted |
{ order } |
Each submission attempt |
OrderConfirmed |
OrderResponse |
Order placed |
OrderFailed |
{ status, error, orderId?, confirmationNumber?, quoteId?, itemRef?, shipperId?, vehicle?, order } |
Order rejected, or could not be sent |
Four things to know before you write the handler
1. OrderSubmitted fires once per attempt, not per order. Submission is not
idempotent, so a shipper retrying after a failure produces a second event for the same
order. Deduplicate on quoteId if you record attempts.
2. OrderFailed can arrive with NO preceding OrderSubmitted. Two cases reject the
order before anything is sent — an invalid contact, and a widget that has lost its
api-config. If your handler marks an order "pending" on OrderSubmitted and resolves it
on OrderConfirmed/OrderFailed, it will receive a resolution for something it never
marked pending. Key your state off the order itself, not off having seen OrderSubmitted.
3. pbc:error tells you the widget is unavailable; QuoteError tells you why. They
are different signals and both can fire for one failure. Listen for QuoteError if you
want to distinguish a bad postal code from an expired token from an outage.
4. Match an outcome to your request with its correlation fields. quote_bus is shared
by the whole page, so every tab or modal hears every OrderConfirmed and OrderFailed.
Both carry quoteId, itemRef, shipperId and vehicle at the top level, copied from the
order that was submitted — the same values OrderSubmitted carries in order. Compare
them with what you supplied and ignore events that aren't yours. Values are echoed exactly:
a field you left out of quote-request is absent, and one you sent as '' comes back as
''. itemRef, shipperId and vehicle.vin are your own values, so they are the ones
to match on. quoteId is the quote the shipper picked, which you supplied nothing for; it
is useful only if you captured it from that widget's OrderSubmitted.
A business rejection from the order service passes its whole response through, so an
OrderFailed may also carry orderId and confirmationNumber. Treat both as optional on
either event — don't write a type guard that rejects an OrderFailed that has them.
error.code values you may see on OrderFailed
| Code | Meaning | Was anything sent? |
|---|---|---|
invalid_contact |
Phone or email failed validation | No |
not_configured |
Widget lost its api-config before submitting |
No |
service_unavailable |
The order service could not be reached | Yes, outcome unknown |
| (from the BFF) | Business rejection — code and message come from the service | Yes, rejected |
service_unavailable is the only one where the order may have reached the service, so
treat it as indeterminate rather than as a clean failure.
Privacy note.
OrderSubmittedandOrderFailedcarry the full order, which includes the contact name, phone and email — deliberately, since you own the contact and need it to fulfil the order. Once you setuser-email, that address rides along too. Treat those events as containing personal data when deciding what to log.
Response shapes
MultiQuoteResponse {
quotes: { // always contains all three keys
open?: TransportPriceOption,
enclosed?: TransportPriceOption,
expedited?: TransportPriceOption,
},
isHighValueVehicle: boolean,
status: 'Successful' | 'Partial' | 'Failed',
}
TransportPriceOption {
price: number | null, // null when this option could not be priced
currency: string | null,
minDaysToDeliver: number | null,
maxDaysToDeliver: number | null,
expirationDate: string | null, // UTC
quoteId: string | null,
error: { code, message } | null,
}
OrderResponse {
orderId?: string,
confirmationNumber?: string,
status: 'Unknown' | 'Confirmed' | 'Failed',
error?: { code, message },
// Correlation fields, echoed from the submitted order:
quoteId?: string,
itemRef?: string,
shipperId?: string,
vehicle?: { vin?, isOperable, description?: { year?, make?, model?, subModel? } },
}
A Partial status is normal, not an error — one option can price while another fails.
The widget handles the display; you only need status if you record the outcome.
Framework notes
React
import { useEffect, useRef } from 'react';
export function QuotePage({ apiConfig, quoteRequest, contact, userEmail }) {
const ref = useRef(null);
// Object props MUST be assigned as DOM properties. React sets unknown JSX props as
// attributes, so an object would arrive as the string "[object Object]".
useEffect(() => {
const el = ref.current;
if (!el) return;
el.apiConfig = apiConfig;
el.quoteRequest = quoteRequest;
el.contact = contact;
el.orderSource = 'OVE';
el.customerId = '5000001';
el.userEmail = userEmail; // optional
}, [apiConfig, quoteRequest, contact, userEmail]);
useEffect(() => {
const channel = new BroadcastChannel('quote_bus');
channel.onmessage = ({ data }) => {
if (data.eventType === 'OrderConfirmed') {
console.log('Confirmation:', data.eventBody.confirmationNumber);
}
};
return () => channel.close();
}, []);
return <quote-order-component ref={ref} />;
}
Angular
@ViewChild('quote') quote!: ElementRef;
ngAfterViewInit() {
const el = this.quote.nativeElement;
el.apiConfig = this.apiConfig;
el.quoteRequest = this.quoteRequest;
el.orderSource = 'OVE';
el.customerId = '5000001';
el.userEmail = this.userEmail;
}
Add CUSTOM_ELEMENTS_SCHEMA to your module so Angular accepts the unknown element.
Sizing
Responsive between 320px and 560px, using CSS container queries — it responds to the width of your container, not the browser window. Give it a sized wrapper:
.quote-widget-container { max-width: 560px; }
Do not try to restyle the interior; Shadow DOM makes that unreliable by design. Raise visual changes with the team.
Troubleshooting
| What you see | Why |
|---|---|
| "This transportation quote is currently unavailable. Please try again later." | The token in api-config is missing, expired or invalid, or quote-request is missing the move details: at least two stops and one vehicle. Fix your configuration; the shipper cannot resolve this by calling. |
| "Unable to retrieve transportation pricing at this time." with a Contact Ready Logistics link | No transport option could be priced for this move, or the pricing service failed. The configuration is accepted; the shipper can try again or call. |
| A message under a field (ZIP code, phone, email) | That input is invalid. The shipper corrects it and continues; nothing is sent until it is valid. |
Nothing renders. Check the console for a failed script load, then confirm pbc:ready
fired. If you got pbc:error with reason: 'unavailable', the widget loaded but rejected
its configuration — usually a missing or expired token.
Unavailable state instead of the widget. api-config missing/malformed or its token
invalid, or quote-request failed validation — it needs at least two stops and one vehicle.
Object props arriving empty. In a framework you are probably setting an attribute rather than a property. See the React note above.
A phone number you consider valid is rejected. Validation is NANP-structural: area code and exchange must start 2–9 and neither may be an N11 code.
Quotes return but an option has no price. Expected — check quotes.<key>.error. A
Partial status means some options priced and others did not. Expedited is also withheld
entirely for an OS vehicle.
Before you start — what to request
- A bearer token, and how to refresh it
- Your
partnerId,orderSourceandcustomerIdvalues - Which environment your testing should point at
What you do not need to ask us for: the script and BFF URLs are in the tables above.
userEmail comes from your own session rather than from us — but work out where you will
read the signed-in user's address from before you start.
Component reference (generated)
Generated from the components' source code when this version was built. Messages on the quote_bus BroadcastChannel are not DOM events: see the event sections of this guide.
<ready-zipcode-quote>
| Property | Attribute | Type | Default | Required | Description |
|---|---|---|---|---|---|
apiConfig | api-config | bffConfigs | undefined | — | No | — |
quoteRequest | quote-request | quoteRequest | undefined | — | No | — |
vdpMode | vdp-mode | boolean | false | No | Renders for embedding in a vehicle detail page whose parent application owns
the ZIP code (F306320). The ZIP field and the GO button are hidden, and the
quote is generated from the ZIP carried on Defaults to |
wideBreakpoint | wide-breakpoint | number | 800 | No | Width, in CSS pixels, from which the card switches from the stacked layout (ZIP entry above the quote) to a single short strip (quote beside it). Measured against this element's own content box, never the viewport, so one page can embed a narrow and a wide instance side by side. Defaults to 800. The strip's type is sized so the whole quote fits on one line from roughly 780px; a smaller value is accepted, but below that the delivery text wraps and the strip grows taller. A value that is not a number falls back to the default. |
No DOM events.
<ready-contact-quote>
The OVE transportation quote (F318368).
An address edit only takes effect on a successful quote. The quote is requested with a copy of the edited stop; the saved address and quote are left exactly as they were until a successful response replaces both. So:
- success → save the address (City/State from the response) and the quote, close the form
- ZIP error → keep the form open with what the user typed, error under the ZIP (AC6, AC8)
- address too long (host-supplied on load, or the service's 400 after an edit) → form open with the message on the field; Cancel shows it in place of the price (US2151524)
- any other failure → close the form, keep the saved address, show the service-down message in place of the price (AC7)
- Cancel, including while the request is in flight → back to the saved address and quote;
the request is aborted so no
QuoteCreatedis published (AC10, N2)
| Property | Attribute | Type | Default | Required | Description |
|---|---|---|---|---|---|
apiConfig | api-config | bffConfigs | undefined | — | No | — |
quoteId | quote-id | string | undefined | — | No | — |
quoteRequest | quote-request | quoteRequest | undefined | — | No | — |
No DOM events.
<quote-order-component>
Public PBC widget for the Quote/Order experience (F255314). Owns the screen
FSM and drives the internal sub-components; communicates with the host via
the quote_bus BroadcastChannel. US1: options selection + requote.
| Property | Attribute | Type | Default | Required | Description |
|---|---|---|---|---|---|
apiConfig | api-config | bffConfigs | undefined | — | No | — |
contact | contact | OrderContact | undefined | — | No | — |
contactRequired | contact-required | boolean | false | No | Does the host require a contact? Public attribute —
Presence-based, like every native boolean attribute — All four spellings work, because Stencil coerces the attribute value: omitted → false (the default) contact-required → true (bare — the idiomatic form) contact-required="true" → true contact-required="false" → false That last row is a Stencil convenience, not standard HTML. Stencil's
The residual risk is narrow but real: a host that intends required mode and simply forgets the attribute gets optional mode, and the widget cannot tell the difference. Host-facing documentation and one integration test are the mitigation. |
customerId | customer-id | string | undefined | — | No | — |
orderSource | order-source | string | undefined | — | No | — |
quoteContact | quote-contact | contact | undefined | — | No | — |
quoteRequest | quote-request | quoteRequest | undefined | — | No | — |
userEmail | user-email | string | undefined | — | No | Acting user who accepted the on-screen disclosures. Public attribute —
OPTIONAL, with a documented default (US2097771 / FR-002): omit it and the widget loads, quotes and orders exactly as today, and the acceptance note is still written — just without attribution (FR-010). That default is what satisfies UI-4, which requires new attributes to be optional. Absence is a normal path, so do not add a presence check, a validation rule, or a submit gate for it. Team decision 2026-09-22. This reversed twice during specification; the "required" reading made the release MAJOR, needed a UI-4 override, and broke 31 existing tests. See specs/US2097771-order-acknowledgments-caveat/research.md R9 before changing it. Host-asserted, never verified. NOT derived from the bearer token — that is a client-credentials token identifying the calling application, carrying no user claim (UI-8). FR-003 therefore forbids presenting or relying on this as a verified identity, and the note must not assert the person's relationship to the customer. Privacy: the value enters the DOM, so session replay captures it, in addition to being visible to dispatch and carrier staff via the order note. Both reviewed and accepted by the business (spec Assumption 2). |
No DOM events.