{
  "info": {
    "name": "project44 \u2014 LTL Rating & Dispatch (v4, NA Production)",
    "description": "Public **LTL Rating & Dispatch** API for project44, covering the full lifecycle: rate a shipment, book it with the carrier, retrieve documents and cancel.\n\n### Setup\n\n1. Select the **project44 LTL \u2014 NA Production** environment.\n2. Set `clientId` and `clientSecret` to your OAuth2 client application credentials.\n3. Run **1. Setup \u2192 List Account Groups**, then **List Accounts in a Group**. These fill in\n   `accountGroupCode`, `accountCode` and `carrierCode` for you.\n4. Run the remaining folders in order \u2014 each one hands variables to the next.\n\n### Account groups\n\nEvery rating, dispatch, cancellation and image request carries a `capacityProviderAccountGroup`.\nIts `code` names a group configured in the project44 Self-Service Portal (not through this API);\nomitting it falls back to the group named `Default`. The group bundles your carrier accounts \u2014\nthe negotiated credentials project44 uses when it talks to each carrier on your behalf.\n\n`accounts` means different things per operation: rating and transit time treat an empty or absent\nlist as *all accounts in the group*, while dispatch, cancellation and images require **exactly\none**.\n\n### Environment\n\nTargets **NA production** only: `https://na12.api.project44.com`.\n\n### Authentication\n\nOAuth2 **client credentials**. A collection-level pre-request script fetches a bearer token on first use and caches it until a minute before expiry, so you do not need to send *Get OAuth2 Token* yourself.\n\nThe tenant is implied by the client application. Internal headers such as `X-Tenant-Purpose` are not part of the public contract \u2014 do not send them.\n\n### Before you run the Dispatch folder\n\nThis environment points at NA **production**. *Dispatch Shipment* books a real shipment with a\nreal carrier, and *Cancellation* cancels one. No idempotency mechanism is documented for either,\nand the dispatch body uses `BOL-{{$timestamp}}`, so sending it twice books two shipments rather\nthan being rejected as a duplicate. Rating, transit time and the Setup folder are all read-only\nand safe to run freely.\n\n### Rate limits\n\n600 requests per minute, 600 burst, per organization account **and** per IP, with 5 per minute on\nthe token endpoint. Exceeding either returns `429`. No `X-RateLimit-*` headers or reset semantics\nare documented, so there is nothing to read back \u2014 the collection logs a clear message on `429`\nand the guidance is simply to spread requests out. project44 can raise the limit on request.\n\n### The one thing that surprises most integrators\n\nA `200` from the rating endpoint **does not mean every carrier succeeded**. `rateQuotes` includes failed carriers as sparse elements carrying only `carrierCode`, `capacityProviderAccountGroup` and `errorMessages` \u2014 with no `rateQuoteDetail`, so no price. Always check for `rateQuoteDetail` before reading a total. The rating requests ship a saved example of this and log every failed carrier to the Postman console.\n\nDispatch behaves differently: its confirmation has no `errorMessages` field at all, and a failure arrives as a non-2xx status with an `ApiError` body.\n\n### Request bodies\n\nShapes are modelled on real NA production traffic so the field combinations match what integrators actually send. All identities, contacts and street addresses are fabricated.\n",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{accessToken}}",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Runs before every request in this collection.",
          "//",
          "// 1. Keeps pickupDate / deliveryDate in the future. Carriers reject a ship date",
          "//    earlier than today, so hardcoded dates would make this collection rot.",
          "const pad = n => String(n).padStart(2, \"0\");",
          "const iso = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;",
          "const plusDays = (n, from) => { const d = from ? new Date(from + \"T00:00:00\") : new Date(); d.setDate(d.getDate() + n); return iso(d); };",
          "",
          "// ISO yyyy-mm-dd strings compare correctly as strings. Replace a stored date that has gone stale,",
          "// but keep a valid user-chosen future date.",
          "const pickup = pm.environment.get(\"pickupDate\");",
          "if (!pickup || pickup < plusDays(1)) { pm.environment.set(\"pickupDate\", plusDays(1)); }",
          "const delivery = pm.environment.get(\"deliveryDate\");",
          "if (!delivery || delivery <= pm.environment.get(\"pickupDate\")) { pm.environment.set(\"deliveryDate\", plusDays(3, pm.environment.get(\"pickupDate\"))); }",
          "",
          "// 2. Fetches an OAuth2 access token when the cached one is missing or expired.",
          "//",
          "// Skip when the request being sent IS the token request -- otherwise sending it by",
          "// hand costs two token calls, and the endpoint allows only 5 per minute.",
          "if (pm.info.requestName === \"Get OAuth2 Token\") { return; }",
          "",
          "const expiry = Number(pm.environment.get(\"tokenExpiresAt\") || 0);",
          "const token  = pm.environment.get(\"accessToken\");",
          "",
          "if (token && Date.now() < expiry - 60000) { return; }   // 60s safety margin",
          "",
          "const clientId     = pm.environment.get(\"clientId\");",
          "const clientSecret = pm.environment.get(\"clientSecret\");",
          "",
          "if (!clientId || !clientSecret) {",
          "    console.log(\"Set clientId and clientSecret in your environment before sending requests.\");",
          "    return;",
          "}",
          "",
          "pm.sendRequest({",
          "    url: pm.environment.get(\"baseUrl\") + \"/api/v4/oauth2/token\",",
          "    method: \"POST\",",
          "    header: { \"Content-Type\": \"application/x-www-form-urlencoded\" },",
          "    body: {",
          "        mode: \"urlencoded\",",
          "        urlencoded: [",
          "            { key: \"client_id\",     value: clientId },",
          "            { key: \"client_secret\", value: clientSecret },",
          "            { key: \"grant_type\",    value: \"client_credentials\" }",
          "        ]",
          "    }",
          "}, (err, res) => {",
          "    // Throwing from an async callback is not a reliable abort in the Postman sandbox, so",
          "    // log and leave the token empty -- the request then fails visibly with a 401.",
          "    if (err) { console.log(\"Token request failed: \" + err); return; }",
          "    if (res.code !== 200) {",
          "        console.log(`Token request returned ${res.code}: ${res.text()}`);",
          "        return;",
          "    }",
          "    const data = res.json();",
          "    pm.environment.set(\"accessToken\", data.access_token);",
          "    pm.environment.set(\"tokenExpiresAt\", Date.now() + (data.expires_in * 1000));",
          "});"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Runs after every request in this collection.",
          "//",
          "// There is no refresh token: the portal's guidance is to regenerate after a 401.",
          "// Dropping the cached token means the next send fetches a fresh one, which recovers",
          "// from a revoked or rotated credential without anyone having to clear it by hand.",
          "if (pm.response.code === 401) {",
          "    pm.environment.set(\"accessToken\", \"\");",
          "    pm.environment.set(\"tokenExpiresAt\", \"0\");",
          "    console.log(\"401 received -- cached token discarded. Re-send to obtain a fresh one.\");",
          "    console.log(\"If it keeps happening, check clientId/clientSecret and that the client \" +",
          "                \"application is still active.\");",
          "}",
          "",
          "// 600 requests/minute per organization account and per IP; 5/minute on the token endpoint.",
          "// No X-RateLimit-* headers or reset semantics are documented, so there is nothing to read",
          "// back -- the only guidance is to spread requests out.",
          "if (pm.response.code === 429) {",
          "    console.log(\"429 rate limited. The documented limits are 600 requests/minute per \" +",
          "                \"organization account and IP, and 5/minute on /oauth2/token.\");",
          "    console.log(\"Wait a minute before retrying, and avoid large bursts. Contact project44 \" +",
          "                \"if you need the limit raised.\");",
          "}"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "rateQuoteId",
      "value": "",
      "type": "string"
    },
    {
      "key": "capacityProviderQuoteNumber",
      "value": "",
      "type": "string"
    },
    {
      "key": "shipmentId",
      "value": "",
      "type": "string"
    },
    {
      "key": "proNumber",
      "value": "",
      "type": "string"
    },
    {
      "key": "pickupNumber",
      "value": "",
      "type": "string"
    },
    {
      "key": "cancelIdType",
      "value": "PICKUP",
      "type": "string"
    },
    {
      "key": "cancelIdValue",
      "value": "",
      "type": "string"
    },
    {
      "key": "billOfLading",
      "value": "",
      "type": "string"
    },
    {
      "key": "imageUrl",
      "value": "",
      "type": "string"
    },
    {
      "key": "bolUrl",
      "value": "",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "0. Authentication",
      "item": [
        {
          "name": "Get OAuth2 Token",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/x-www-form-urlencoded"
              }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "grant_type",
                  "value": "client_credentials",
                  "type": "text"
                },
                {
                  "key": "client_id",
                  "value": "{{clientId}}",
                  "type": "text"
                },
                {
                  "key": "client_secret",
                  "value": "{{clientSecret}}",
                  "type": "text"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/api/v4/oauth2/token",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v4",
                "oauth2",
                "token"
              ]
            },
            "description": "Exchanges client application credentials for a bearer token.\n\nCredentials may be sent **either** as `client_id` / `client_secret` form fields (what this request does) **or** as HTTP Basic auth \u2014 but **never both at once**. Sending both returns `401`, which is why this request's Authorization is set to *No Auth* rather than inheriting the collection's bearer token.\n\n**Rate limit: 5 token requests per minute** (50 burst), per client and per IP. The collection caches the token, so you will not hit this in normal use \u2014 but a Collection Runner against a cleared environment, or several engineers sharing one client id, can.\n\nTokens currently last about 12 hours, though the portal notes that is subject to change, so the pre-request script trusts the `expires_in` field rather than hardcoding a lifetime. There is **no refresh token**: a token is replaced by requesting a new one.\n\n`grant_type` accepts only `client_credentials`. project44 does not support grant types that involve end-user consent.\n\nThe tenant is implied by the client application \u2014 it is **not** passed as a header. Internal headers such as `X-Tenant-Purpose` are not part of the public contract and must not be sent.\n\nEvery other request in this collection runs a collection-level pre-request script that calls this endpoint automatically and caches the token until it expires, so you normally never need to send this request by hand."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Token issued\", () => pm.response.to.have.status(200));",
                  "",
                  "if (pm.response.code !== 200) {",
                  "    console.log(`Token request failed (${pm.response.code}): ${pm.response.text()}`);",
                  "    return;",
                  "}",
                  "",
                  "const data = pm.response.json();",
                  "pm.environment.set(\"accessToken\", data.access_token);",
                  "pm.environment.set(\"tokenExpiresAt\", Date.now() + (data.expires_in * 1000));",
                  "console.log(`Token cached, expires in ${data.expires_in}s (${data.token_type || \"BEARER\"}).`);"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "1. Setup \u2014 Account Groups & Carriers",
      "item": [
        {
          "name": "List Account Groups",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v4/capacityprovideraccountgroups",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v4",
                "capacityprovideraccountgroups"
              ]
            },
            "description": "Lists every capacity provider account group available to your tenant. **Start here** \u2014 the `code` of one of these is what belongs in the `accountGroupCode` environment variable, and it is required by every rating, dispatch, cancellation and image request.\n\nAccount groups are created in the project44 Self-Service Portal, not through this API. A group bundles the carrier accounts (your negotiated credentials with each carrier) that an operation should run against.\n\nIf you never set `code`, project44 falls back to the group literally named `Default`.\n\n**Selecting one.** The test prints a numbered list and then:\n\n* if `accountGroupCode` already names a group in the list, it is left alone \u2014 re-running this request never silently changes your selection;\n* if you have exactly one group, it is selected for you;\n* if you have several and none is chosen yet, nothing is set \u2014 pick a `code` from the console output and put it in the `accountGroupCode` environment variable yourself.\n\nThat last case is deliberate. Which account group you quote against determines which carriers and which negotiated rates you get, so it is not a choice worth guessing at."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "",
                  "const groups  = pm.response.json().groups || [];",
                  "const current = pm.environment.get(\"accountGroupCode\");",
                  "",
                  "console.log(`${groups.length} account group(s) available:",
                  "`);",
                  "groups.forEach((g, i) => {",
                  "    const mark = g.code === current ? \" <-- selected\" : \"\";",
                  "    console.log(`  [${i + 1}] ${g.code}${g.name ? \"  (\" + g.name + \")\" : \"\"}${mark}`);",
                  "});",
                  "",
                  "if (!groups.length) {",
                  "    console.log(\"None found. Account groups are created in the project44 Self-Service Portal.\");",
                  "} else if (current && groups.some(g => g.code === current)) {",
                  "    // Your choice stands. Re-running this request must never silently change it.",
                  "    console.log(`",
                  "Keeping accountGroupCode = ${current}`);",
                  "} else if (groups.length === 1) {",
                  "    pm.environment.set(\"accountGroupCode\", groups[0].code);",
                  "    console.log(`",
                  "Only one group, so selected it: accountGroupCode = ${groups[0].code}`);",
                  "} else {",
                  "    console.log(`",
                  "${groups.length} groups to choose from -- none selected yet.`);",
                  "    console.log(\"Set the `accountGroupCode` environment variable to one of the codes above,\");",
                  "    console.log(\"then run `List Accounts in a Group`.\");",
                  "    if (current) { console.log(`",
                  "(Current value \"${current}\" does not match any group above.)`); }",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "List Accounts in a Group",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v4/capacityprovideraccounts?accountGroupCode={{accountGroupCode}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v4",
                "capacityprovideraccounts"
              ],
              "query": [
                {
                  "key": "accountGroupCode",
                  "value": "{{accountGroupCode}}"
                }
              ]
            },
            "description": "Lists the carrier accounts inside one account group.\n\nEach entry has two identifiers that are easy to confuse. `code` is the **account-set code** you chose when the account was created \u2014 that is what goes in `accounts[].code`. `capacityProviderIdentifier` holds the carrier's own identifier, usually a **SCAC** such as `ABFS` or `SAIA`, and that is what a dispatch request's top-level `carrierCode` wants. They are frequently the same string, which is exactly why the distinction bites later.\n\nUse this to find out which carriers you can actually quote and dispatch with, and to pick a value for `accounts[].code` on the dispatch, cancellation and image requests \u2014 those require **exactly one** account, unlike rating which accepts many or none.\n\nOmit the `accountGroupCode` query parameter to list accounts across all groups.\n\n**Selecting one.** Set the `preferredCarrier` environment variable to a SCAC (for example `ABFS`) and re-run \u2014 the matching account is selected. Otherwise an existing `accountCode` is kept, a lone account is selected automatically, and a list of several leaves the choice to you.\n\nRating can query every account at once, so you only strictly need to choose here before dispatching, cancelling or pulling documents."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "",
                  "const accounts  = pm.response.json().accounts || [];",
                  "const current   = pm.environment.get(\"accountCode\");",
                  "const preferred = (pm.environment.get(\"preferredCarrier\") || \"\").trim().toUpperCase();",
                  "",
                  "// The identifier may be SCAC, DOT_NUMBER, MC_NUMBER, P44_EU, SYSTEM, P44_GLOBAL or VAT.",
                  "// Only a SCAC belongs in carrierCode, which the spec defines as the carrier's SCAC.",
                  "const idOf   = a => a.capacityProviderIdentifier || {};",
                  "const scacOf = a => (idOf(a).type === \"SCAC\" ? idOf(a).value : \"\") || \"\";",
                  "const anyIdOf = a => idOf(a).value ? `${idOf(a).type}=${idOf(a).value}` : \"?\";",
                  "",
                  "// `code` is the account-set code -- that is what goes in accounts[].code.",
                  "// `capacityProviderIdentifier.value` is the carrier SCAC -- that is the",
                  "// top-level `carrierCode` on a dispatch request. They often match, but not always.",
                  "console.log(`${accounts.length} account set(s) in group ` +",
                  "            `${pm.environment.get(\"accountGroupCode\")}:",
                  "`);",
                  "accounts.forEach((a, i) => {",
                  "    const mark = a.code === current ? \" <-- selected\" : \"\";",
                  "    console.log(`  [${i + 1}] account=${a.code}  ${anyIdOf(a)}${mark}`);",
                  "});",
                  "",
                  "const select = (a, why) => {",
                  "    pm.environment.set(\"accountCode\", a.code);",
                  "    if (scacOf(a)) { pm.environment.set(\"carrierCode\", scacOf(a)); }",
                  "    console.log(`",
                  "${why}: accountCode = ${a.code}, carrierCode = ${scacOf(a) || \"(unset)\"}`);",
                  "};",
                  "",
                  "if (!accounts.length) {",
                  "    console.log(\"None found. Check accountGroupCode, or add accounts in the Self-Service Portal.\");",
                  "} else if (preferred) {",
                  "    // An explicit `preferredCarrier` always wins -- that is the point of setting it.",
                  "    const hit = accounts.find(a => scacOf(a).toUpperCase() === preferred ||",
                  "                                   (a.code || \"\").toUpperCase() === preferred);",
                  "    if (hit) { select(hit, `Matched preferredCarrier \"${preferred}\"`); }",
                  "    else {",
                  "        console.log(`",
                  "preferredCarrier \"${preferred}\" matches no account above -- nothing changed.`);",
                  "    }",
                  "} else if (current && accounts.some(a => a.code === current)) {",
                  "    console.log(`",
                  "Keeping accountCode = ${current}`);",
                  "} else if (accounts.length === 1) {",
                  "    select(accounts[0], \"Only one account, so selected it\");",
                  "} else {",
                  "    console.log(`",
                  "${accounts.length} accounts to choose from -- none selected yet.`);",
                  "    console.log(\"Either set `preferredCarrier` in the environment to a SCAC from the list above\");",
                  "    console.log(\"and re-run this request, or set `accountCode` and `carrierCode` by hand.\");",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "List Capacity Providers",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v4/capacityproviders",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v4",
                "capacityproviders"
              ]
            },
            "description": "Lists every carrier project44 supports, whether or not you have an account with them. Reference data \u2014 useful when deciding which carrier accounts to set up in the Self-Service Portal.\n\nThis is **not** the list you can quote against; for that use *List Accounts in a Group*."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "const providers = pm.response.json().capacityProviders || [];",
                  "console.log(`${providers.length} supported capacity provider(s)`);",
                  "providers.slice(0, 20).forEach(p => {",
                  "    const ids = (p.capacityProviderIdentifiers || []).map(i => `${i.type}=${i.value}`).join(\" \");",
                  "    console.log(`  ${p.name || \"(unnamed)\"}  ${ids}`);",
                  "});",
                  "if (providers.length > 20) { console.log(`  ... and ${providers.length - 20} more`); }"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2. Rating",
      "item": [
        {
          "name": "Rate Quote \u2014 full addresses + enhanced handling units",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v4/ltl/quotes/rates/query",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v4",
                "ltl",
                "quotes",
                "rates",
                "query"
              ]
            },
            "description": "Requests rates from every carrier in the account group.\n\nThis variant sends **both** `lineItems` and `enhancedHandlingUnits`. `enhancedHandlingUnits` is the more granular representation and enables order-level tracking, more accurate rates and more detailed BOLs; sending both is explicitly accepted, and is common when migrating.\n\n\u26a0\ufe0f **`enhancedHandlingUnits` is gated.** The spec notes it is *\"currently not supported by all vendors\"* and the operation description asks you to contact your project44 account manager before using it. If you have not, use the *postal codes only* request, which sends `lineItems` alone.\n\n`accounts` is empty here, which combined with `apiConfiguration.fallBackToDefaultAccountGroup: true` tells project44 to use the account group's default carrier set.\n\nOn success this request stores `rateQuoteId`, `carrierCode` and `capacityProviderQuoteNumber` as collection variables, which the dispatch request consumes.\n\n**`capacityProviderAccountGroup`**\n\n`code` names an account group configured in the project44 Self-Service Portal. If you omit it, the group named `Default` is used. Run *List Account Groups* in the Setup folder to see yours.\n\n`accounts` behaves differently per operation:\n\n| Operation | `accounts` |\n| --- | --- |\n| Rating, transit time | optional \u2014 empty or omitted means **all** accounts in the group |\n| Dispatch, cancellation, images | **exactly one** account is required |\n\nTwo identifiers are easy to confuse. `accounts[].code` is the **account-set code** you chose in the Self-Service Portal. The top-level `carrierCode` on a dispatch request is the carrier's **SCAC**. They are frequently the same string (`ABFS`, `SAIA`, `FXFE`), which hides the difference until the day it bites \u2014 *List Accounts in a Group* prints both side by side.\n\n---\n\n### A `200` does not mean every carrier succeeded\n\n`rateQuotes` contains one element per capacity provider that was queried \u2014 including the ones that **failed**. A failed element carries only `capacityProviderAccountGroup`, `carrierCode` and `errorMessages`. Everything else is absent: `rateQuoteDetail`, `serviceLevel`, `transitDays`, `transitDaysRange`, `deliveryDateTime`, `currencyCode`, `laneType`, `alternateRateQuotes` and `capacityProviderQuoteNumber`.\n\nIf every carrier fails, the response is still `200` with a `rateQuotes` array in which no element has a price. **Always check for `rateQuoteDetail` before reading a total.**\n\nSee the saved *200 \u2014 partial success* example on this request, and the Tests tab, which logs each failed carrier's `message` and `diagnostic`.\n\n`errorMessages` carries only `severity: ERROR`; `infoMessages` carries only `INFO` and `WARNING`. `source` distinguishes `SYSTEM` (project44) from `CAPACITY_PROVIDER` (the carrier).\n\nEach error message may also carry an `errorCode`: the normalized project44 error code, identical across carriers. Branch on it rather than parsing `message` or `diagnostic`. It is omitted when no code is available, and clients must tolerate the field being present.\n\nTo be precise about what is and is not documented: the `errorMessages` field itself **is** in the published schema, it is just invisible on the rendered portal page because `rateQuotes` collapses behind a *+Show 21 array properties* expander. What is genuinely undocumented is the behaviour around it \u2014 that an all-carriers-fail response is still `200`, and that a failed element omits the pricing and service fields entirely. Both were confirmed against production traffic rather than inferred.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"originAddress\": {\n    \"addressLines\": [\n      \"1200 Industrial Parkway\"\n    ],\n    \"city\": \"Barrington\",\n    \"state\": \"IL\",\n    \"postalCode\": \"60010\",\n    \"country\": \"US\"\n  },\n  \"destinationAddress\": {\n    \"addressLines\": [\n      \"4500 Commerce Drive\"\n    ],\n    \"city\": \"Bowling Green\",\n    \"state\": \"KY\",\n    \"postalCode\": \"42104\",\n    \"country\": \"US\"\n  },\n  \"lineItems\": [\n    {\n      \"totalWeight\": 516,\n      \"packageDimensions\": {\n        \"length\": 48,\n        \"width\": 40,\n        \"height\": 43\n      },\n      \"freightClass\": \"92.5\",\n      \"packageType\": \"PLT\",\n      \"totalPackages\": 1,\n      \"totalPieces\": 11,\n      \"description\": \"ADHESIVES AND SEALANTS\",\n      \"stackable\": true,\n      \"nmfcItemCode\": \"46030\",\n      \"nmfcSubCode\": \"02\"\n    }\n  ],\n  \"enhancedHandlingUnits\": [\n    {\n      \"pickupStopNumber\": 1,\n      \"deliveryStopNumber\": 2,\n      \"weightPerHandlingUnit\": 516,\n      \"packages\": [\n        {\n          \"description\": \"ADHESIVES AND SEALANTS\",\n          \"weightPerPackage\": 516,\n          \"freightClass\": \"92.5\",\n          \"nmfcCodes\": [\n            {\n              \"code\": \"046030-02\"\n            }\n          ],\n          \"packageQuantity\": 1\n        }\n      ],\n      \"stackable\": true,\n      \"handlingUnitQuantity\": 1,\n      \"handlingUnitType\": \"PALLET\",\n      \"handlingUnitDimensions\": {\n        \"length\": 48,\n        \"width\": 40,\n        \"height\": 43\n      },\n      \"freightClasses\": [\n        \"92.5\"\n      ]\n    }\n  ],\n  \"capacityProviderAccountGroup\": {\n    \"code\": \"{{accountGroupCode}}\",\n    \"accounts\": []\n  },\n  \"accessorialServices\": [\n    {\n      \"code\": \"LGDEL\"\n    },\n    {\n      \"code\": \"INDEL\"\n    }\n  ],\n  \"pickupWindow\": {\n    \"date\": \"{{pickupDate}}\",\n    \"startTime\": \"08:00\",\n    \"endTime\": \"16:00\"\n  },\n  \"weightUnit\": \"LB\",\n  \"lengthUnit\": \"IN\",\n  \"paymentTermsOverride\": \"PREPAID\",\n  \"directionOverride\": \"THIRD_PARTY\",\n  \"preferredCurrency\": \"USD\",\n  \"preferredSystemOfMeasurement\": \"IMPERIAL\",\n  \"apiConfiguration\": {\n    \"timeout\": 10,\n    \"accessorialServiceConfiguration\": {\n      \"fetchAllServiceLevels\": true\n    },\n    \"fallBackToDefaultAccountGroup\": true\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "",
                  "const quotes = pm.response.json().rateQuotes || [];",
                  "const priced = quotes.filter(q => q.rateQuoteDetail);",
                  "const failed = quotes.filter(q => q.errorMessages && q.errorMessages.length);",
                  "",
                  "pm.test(\"At least one carrier returned a price\", () => pm.expect(priced).to.not.be.empty);",
                  "",
                  "// A 200 does NOT mean every carrier succeeded. Failed carriers are returned in the",
                  "// same array with only carrierCode, capacityProviderAccountGroup and errorMessages --",
                  "// rateQuoteDetail, serviceLevel, transitDays and currencyCode are all absent.",
                  "if (failed.length) {",
                  "    console.log(`${failed.length} of ${quotes.length} carrier(s) failed:`);",
                  "    failed.forEach(q => q.errorMessages.forEach(",
                  "        m => console.log(`  ${q.carrierCode} [${m.source}] ${m.errorCode || \"-\"} ${m.message} -- ${m.diagnostic || \"\"}`)));",
                  "}",
                  "",
                  "if (priced.length) {",
                  "    pm.collectionVariables.set(\"rateQuoteId\", priced[0].id);",
                  "    pm.environment.set(\"carrierCode\", priced[0].carrierCode);",
                  "",
                  "    // accounts[].code (the account-set code) is distinct from carrierCode (the SCAC)",
                  "    const grp = priced[0].capacityProviderAccountGroup || {};",
                  "    if ((grp.accounts || []).length) {",
                  "        pm.environment.set(\"accountCode\", grp.accounts[0].code);",
                  "    }",
                  "    // Always write this, even when absent -- leaving the previous value would pair a",
                  "    // fresh rateQuoteId with a stale quote number from an earlier request.",
                  "    pm.collectionVariables.set(\"capacityProviderQuoteNumber\",",
                  "                               priced[0].capacityProviderQuoteNumber || \"\");",
                  "    console.log(`Captured rateQuoteId=${priced[0].id} carrier=${priced[0].carrierCode}`);",
                  "}"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 \u2014 partial success (one carrier failed)",
              "status": "OK",
              "code": 200,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "_postman_previewlanguage": "json",
              "body": "{\n  \"rateQuotes\": [\n    {\n      \"capacityProviderAccountGroup\": {\n        \"code\": \"ACCOUNT_GROUP\",\n        \"accounts\": [\n          {\n            \"code\": \"AACT\"\n          }\n        ]\n      },\n      \"carrierCode\": \"AACT\",\n      \"capacityProviderQuoteNumber\": \"0275147128\",\n      \"serviceLevel\": {\n        \"code\": \"STD\",\n        \"description\": \"Standard Rate\"\n      },\n      \"transitDays\": 2,\n      \"transitDaysRange\": {\n        \"min\": 2,\n        \"max\": 2\n      },\n      \"currencyCode\": \"USD\",\n      \"preferredCurrencyCode\": \"USD\",\n      \"rateQuoteDetail\": {\n        \"total\": 174.5,\n        \"totalInPreferredCurrency\": 174.5,\n        \"charges\": [\n          {\n            \"code\": \"ITEM\",\n            \"description\": \"Item Charge\",\n            \"amount\": 1546.9,\n            \"rate\": 3.09\n          },\n          {\n            \"code\": \"DSC\",\n            \"description\": \"DISCOUNT 92.30 PCT\",\n            \"amount\": -1427.79,\n            \"rate\": -92.3\n          },\n          {\n            \"code\": \"FSC\",\n            \"description\": \"FUEL SURCHARGE 46.50 PCT\",\n            \"amount\": 55.39,\n            \"rate\": 46.5\n          }\n        ]\n      },\n      \"laneType\": \"DIRECT\",\n      \"id\": \"9972160883\"\n    },\n    {\n      \"capacityProviderAccountGroup\": {\n        \"code\": \"ACCOUNT_GROUP\",\n        \"accounts\": [\n          {\n            \"code\": \"ABFS\"\n          }\n        ]\n      },\n      \"carrierCode\": \"ABFS\",\n      \"capacityProviderQuoteNumber\": \"LMLPZ92786\",\n      \"serviceLevel\": {\n        \"code\": \"STD\",\n        \"description\": \"Standard Rate\"\n      },\n      \"transitDays\": 1,\n      \"transitDaysRange\": {\n        \"min\": 1,\n        \"max\": 1\n      },\n      \"deliveryDateTime\": \"2026-10-08T00:00:00\",\n      \"currencyCode\": \"USD\",\n      \"preferredCurrencyCode\": \"USD\",\n      \"rateQuoteDetail\": {\n        \"total\": 269.87,\n        \"totalInPreferredCurrency\": 269.87,\n        \"charges\": [\n          {\n            \"code\": \"ITEM\",\n            \"description\": \"Includes all charges and discounts\",\n            \"amount\": 269.87\n          }\n        ]\n      },\n      \"alternateRateQuotes\": [\n        {\n          \"capacityProviderQuoteNumber\": \"LMLPZ92786\",\n          \"serviceLevel\": {\n            \"code\": \"GUR\",\n            \"description\": \"Guaranteed\"\n          },\n          \"deliveryDateTime\": \"2026-10-07T17:00:00\",\n          \"rateQuoteDetail\": {\n            \"total\": 415.0\n          }\n        },\n        {\n          \"capacityProviderQuoteNumber\": \"LMLPZ92786\",\n          \"serviceLevel\": {\n            \"code\": \"GSAM\",\n            \"description\": \"Guaranteed by AM (Noon)\"\n          },\n          \"deliveryDateTime\": \"2026-10-08T12:00:00\",\n          \"rateQuoteDetail\": {\n            \"total\": 456.5\n          }\n        }\n      ],\n      \"laneType\": \"DIRECT\",\n      \"id\": \"9972160884\"\n    },\n    {\n      \"capacityProviderAccountGroup\": {\n        \"code\": \"ACCOUNT_GROUP\",\n        \"accounts\": [\n          {\n            \"code\": \"AVRT\"\n          }\n        ]\n      },\n      \"carrierCode\": \"AVRT\",\n      \"errorMessages\": [\n        {\n          \"severity\": \"ERROR\",\n          \"message\": \"An error occurred while retrieving rates.\",\n          \"diagnostic\": \"The Transit.PickupDate field is invalid. The Ship Date field must not be prior than today's date.\",\n          \"source\": \"CAPACITY_PROVIDER\",\n          \"errorCode\": \"P44_GENERAL\"\n        }\n      ]\n    }\n  ]\n}",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"originAddress\": {\n    \"postalCode\": \"60010\",\n    \"country\": \"US\"\n  },\n  \"destinationAddress\": {\n    \"postalCode\": \"42104\",\n    \"country\": \"US\"\n  },\n  \"lineItems\": [\n    {\n      \"totalWeight\": 227,\n      \"packageDimensions\": {\n        \"length\": 48,\n        \"width\": 40,\n        \"height\": 43\n      },\n      \"freightClass\": \"175\",\n      \"packageType\": \"PLT\",\n      \"totalPackages\": 1,\n      \"totalPieces\": 1,\n      \"description\": \"BOXES, CHESTS OR CABINETS, TOOL, AMMUNITION OR FISHING\",\n      \"stackable\": true,\n      \"nmfcItemCode\": \"29900\",\n      \"nmfcSubCode\": \"04\"\n    }\n  ],\n  \"capacityProviderAccountGroup\": {\n    \"code\": \"{{accountGroupCode}}\",\n    \"accounts\": []\n  },\n  \"pickupWindow\": {\n    \"date\": \"{{pickupDate}}\",\n    \"startTime\": \"16:00\",\n    \"endTime\": \"18:00\"\n  },\n  \"deliveryWindow\": {\n    \"date\": \"{{deliveryDate}}\",\n    \"startTime\": \"08:00\",\n    \"endTime\": \"16:00\"\n  },\n  \"weightUnit\": \"LB\",\n  \"lengthUnit\": \"IN\",\n  \"paymentTermsOverride\": \"PREPAID\",\n  \"directionOverride\": \"SHIPPER\",\n  \"preferredCurrency\": \"USD\",\n  \"apiConfiguration\": {\n    \"timeout\": 50,\n    \"accessorialServiceConfiguration\": {\n      \"allowUnacceptedAccessorials\": true\n    },\n    \"fallBackToDefaultAccountGroup\": true\n  }\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/api/v4/ltl/quotes/rates/query",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "v4",
                    "ltl",
                    "quotes",
                    "rates",
                    "query"
                  ]
                }
              }
            }
          ]
        },
        {
          "name": "Rate Quote \u2014 postal codes only, whole account group",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v4/ltl/quotes/rates/query",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v4",
                "ltl",
                "quotes",
                "rates",
                "query"
              ]
            },
            "description": "The minimum viable rating request: **postal code and country only**, no city, state or street. project44 resolves city and state from the postal code.\n\n`accounts` is empty so every account in the group is quoted, which means this request runs on a clean environment without Setup having chosen anything. To narrow it to one account set, replace `accounts` with `[{\"code\": \"{{accountCode}}\"}]` after running Setup and set `fallBackToDefaultAccountGroup` to `false`. Those are **account-set codes**, not SCACs, even though the two are usually spelled the same.\n\nThis is the variant to use if `enhancedHandlingUnits` is not enabled for your tenant, so it is deliberately free of dependencies on the other rate quote.\n\nThe saved partial-success example lives on the *full addresses* variant rather than here, because it was captured from a request that queried a whole account group \u2014 reusing it on this request would show carriers this body never asked for.\n\n`apiConfiguration.timeout` is in seconds and caps how long project44 waits on carriers. A carrier that does not answer in time comes back as a failed element rather than failing the whole request.\n\n**`capacityProviderAccountGroup`**\n\n`code` names an account group configured in the project44 Self-Service Portal. If you omit it, the group named `Default` is used. Run *List Account Groups* in the Setup folder to see yours.\n\n`accounts` behaves differently per operation:\n\n| Operation | `accounts` |\n| --- | --- |\n| Rating, transit time | optional \u2014 empty or omitted means **all** accounts in the group |\n| Dispatch, cancellation, images | **exactly one** account is required |\n\nTwo identifiers are easy to confuse. `accounts[].code` is the **account-set code** you chose in the Self-Service Portal. The top-level `carrierCode` on a dispatch request is the carrier's **SCAC**. They are frequently the same string (`ABFS`, `SAIA`, `FXFE`), which hides the difference until the day it bites \u2014 *List Accounts in a Group* prints both side by side.\n\n---\n\n### A `200` does not mean every carrier succeeded\n\n`rateQuotes` contains one element per capacity provider that was queried \u2014 including the ones that **failed**. A failed element carries only `capacityProviderAccountGroup`, `carrierCode` and `errorMessages`. Everything else is absent: `rateQuoteDetail`, `serviceLevel`, `transitDays`, `transitDaysRange`, `deliveryDateTime`, `currencyCode`, `laneType`, `alternateRateQuotes` and `capacityProviderQuoteNumber`.\n\nIf every carrier fails, the response is still `200` with a `rateQuotes` array in which no element has a price. **Always check for `rateQuoteDetail` before reading a total.**\n\nSee the saved *200 \u2014 partial success* example on the *full addresses* request, and the Tests tab, which logs each failed carrier's `message` and `diagnostic`.\n\n`errorMessages` carries only `severity: ERROR`; `infoMessages` carries only `INFO` and `WARNING`. `source` distinguishes `SYSTEM` (project44) from `CAPACITY_PROVIDER` (the carrier).\n\nEach error message may also carry an `errorCode`: the normalized project44 error code, identical across carriers. Branch on it rather than parsing `message` or `diagnostic`. It is omitted when no code is available, and clients must tolerate the field being present.\n\nTo be precise about what is and is not documented: the `errorMessages` field itself **is** in the published schema, it is just invisible on the rendered portal page because `rateQuotes` collapses behind a *+Show 21 array properties* expander. What is genuinely undocumented is the behaviour around it \u2014 that an all-carriers-fail response is still `200`, and that a failed element omits the pricing and service fields entirely. Both were confirmed against production traffic rather than inferred.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"originAddress\": {\n    \"postalCode\": \"60010\",\n    \"country\": \"US\"\n  },\n  \"destinationAddress\": {\n    \"postalCode\": \"42104\",\n    \"country\": \"US\"\n  },\n  \"lineItems\": [\n    {\n      \"totalWeight\": 227,\n      \"packageDimensions\": {\n        \"length\": 48,\n        \"width\": 40,\n        \"height\": 43\n      },\n      \"freightClass\": \"175\",\n      \"packageType\": \"PLT\",\n      \"totalPackages\": 1,\n      \"totalPieces\": 1,\n      \"description\": \"BOXES, CHESTS OR CABINETS, TOOL, AMMUNITION OR FISHING\",\n      \"stackable\": true,\n      \"nmfcItemCode\": \"29900\",\n      \"nmfcSubCode\": \"04\"\n    }\n  ],\n  \"capacityProviderAccountGroup\": {\n    \"code\": \"{{accountGroupCode}}\",\n    \"accounts\": []\n  },\n  \"pickupWindow\": {\n    \"date\": \"{{pickupDate}}\",\n    \"startTime\": \"16:00\",\n    \"endTime\": \"18:00\"\n  },\n  \"deliveryWindow\": {\n    \"date\": \"{{deliveryDate}}\",\n    \"startTime\": \"08:00\",\n    \"endTime\": \"16:00\"\n  },\n  \"weightUnit\": \"LB\",\n  \"lengthUnit\": \"IN\",\n  \"paymentTermsOverride\": \"PREPAID\",\n  \"directionOverride\": \"SHIPPER\",\n  \"preferredCurrency\": \"USD\",\n  \"apiConfiguration\": {\n    \"timeout\": 50,\n    \"accessorialServiceConfiguration\": {\n      \"allowUnacceptedAccessorials\": true\n    },\n    \"fallBackToDefaultAccountGroup\": true\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "",
                  "const quotes = pm.response.json().rateQuotes || [];",
                  "const priced = quotes.filter(q => q.rateQuoteDetail);",
                  "const failed = quotes.filter(q => q.errorMessages && q.errorMessages.length);",
                  "",
                  "pm.test(\"At least one carrier returned a price\", () => pm.expect(priced).to.not.be.empty);",
                  "",
                  "// A 200 does NOT mean every carrier succeeded. Failed carriers are returned in the",
                  "// same array with only carrierCode, capacityProviderAccountGroup and errorMessages --",
                  "// rateQuoteDetail, serviceLevel, transitDays and currencyCode are all absent.",
                  "if (failed.length) {",
                  "    console.log(`${failed.length} of ${quotes.length} carrier(s) failed:`);",
                  "    failed.forEach(q => q.errorMessages.forEach(",
                  "        m => console.log(`  ${q.carrierCode} [${m.source}] ${m.errorCode || \"-\"} ${m.message} -- ${m.diagnostic || \"\"}`)));",
                  "}",
                  "",
                  "if (priced.length) {",
                  "    pm.collectionVariables.set(\"rateQuoteId\", priced[0].id);",
                  "    pm.environment.set(\"carrierCode\", priced[0].carrierCode);",
                  "",
                  "    // accounts[].code (the account-set code) is distinct from carrierCode (the SCAC)",
                  "    const grp = priced[0].capacityProviderAccountGroup || {};",
                  "    if ((grp.accounts || []).length) {",
                  "        pm.environment.set(\"accountCode\", grp.accounts[0].code);",
                  "    }",
                  "    // Always write this, even when absent -- leaving the previous value would pair a",
                  "    // fresh rateQuoteId with a stale quote number from an earlier request.",
                  "    pm.collectionVariables.set(\"capacityProviderQuoteNumber\",",
                  "                               priced[0].capacityProviderQuoteNumber || \"\");",
                  "    console.log(`Captured rateQuoteId=${priced[0].id} carrier=${priced[0].carrierCode}`);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Transit Time Quote",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v4/ltl/quotes/transittimes/query",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v4",
                "ltl",
                "quotes",
                "transittimes",
                "query"
              ]
            },
            "description": "Returns estimated transit times without pricing. Needs only the lane and an account group \u2014 no line items.\n\n`accounts` is left empty deliberately: quoting defaults to every account in the group, so this request works before you have picked a specific carrier in Setup.\n\n`transitTimeQuotes` follows the same partial-success rule as rating: a `200` can contain elements carrying only `errorMessages`.\n\n**`capacityProviderAccountGroup`**\n\n`code` names an account group configured in the project44 Self-Service Portal. If you omit it, the group named `Default` is used. Run *List Account Groups* in the Setup folder to see yours.\n\n`accounts` behaves differently per operation:\n\n| Operation | `accounts` |\n| --- | --- |\n| Rating, transit time | optional \u2014 empty or omitted means **all** accounts in the group |\n| Dispatch, cancellation, images | **exactly one** account is required |\n\nTwo identifiers are easy to confuse. `accounts[].code` is the **account-set code** you chose in the Self-Service Portal. The top-level `carrierCode` on a dispatch request is the carrier's **SCAC**. They are frequently the same string (`ABFS`, `SAIA`, `FXFE`), which hides the difference until the day it bites \u2014 *List Accounts in a Group* prints both side by side.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"capacityProviderAccountGroup\": {\n    \"code\": \"{{accountGroupCode}}\",\n    \"accounts\": []\n  },\n  \"originAddress\": {\n    \"addressLines\": [\n      \"1200 Industrial Parkway\"\n    ],\n    \"city\": \"Barrington\",\n    \"state\": \"IL\",\n    \"postalCode\": \"60010\",\n    \"country\": \"US\"\n  },\n  \"destinationAddress\": {\n    \"addressLines\": [\n      \"4500 Commerce Drive\"\n    ],\n    \"city\": \"Bowling Green\",\n    \"state\": \"KY\",\n    \"postalCode\": \"42104\",\n    \"country\": \"US\"\n  },\n  \"pickupDate\": \"{{pickupDate}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "const quotes = pm.response.json().transitTimeQuotes || [];",
                  "const failed = quotes.filter(q => q.errorMessages && q.errorMessages.length);",
                  "if (failed.length) {",
                  "    console.log(`${failed.length} of ${quotes.length} carrier(s) failed:`);",
                  "    failed.forEach(q => q.errorMessages.forEach(m => console.log(`  ${q.carrierCode} ${m.message}`)));",
                  "}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "3. Dispatch",
      "item": [
        {
          "name": "Dispatch Shipment",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v4/ltl/dispatchedshipments",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v4",
                "ltl",
                "dispatchedshipments"
              ]
            },
            "description": "> \u26a0\ufe0f **This books a real shipment with a real carrier.**\n> The environment points at NA **production**, not a sandbox. Every send creates a new booking \u2014 `shipmentIdentifiers` uses `BOL-{{$timestamp}}`, so re-sending does not collide with the previous one, it books a **second shipment**. No idempotency key is documented for this endpoint. If you send it by mistake, use folder 4 to cancel.\n\nBooks the shipment with the carrier. Returns **`201 Created`**.\n\n`rateQuoteId`, `capacityProviderQuoteNumber` and `carrierCode` are filled from whichever rating request you ran last \u2014 run one of those first.\n\nDispatch needs considerably more than rating: full `originLocation` / `destinationLocation` / `requesterLocation` with **contacts**, and `shipmentIdentifiers`.\n\nUnlike rating, the dispatch confirmation has **no** `errorMessages` field \u2014 only `infoMessages`. A dispatch failure surfaces as a non-2xx status with an `ApiError` body, not as a partial success. In JSON, each entry of that body's `errors` list may include the normalized `errorCode`; XML error bodies do not. Do not reuse your rating error-handling here.\n\nThe `201` body also carries `capacityProviderBolUrl` (the carrier's bill of lading, *if available*), `packingVisualizationUrl` and `pickupDateTime`; the test logs all three.\n\n**Which identifier comes back matters.** Nearly all carriers return a pickup confirmation number as `type: PICKUP`; only a few return a `PRO` at dispatch time. The test captures whichever arrived and points the cancellation request at it via `cancelIdType` / `cancelIdValue`. *Query Shipment Images* specifically needs a `PRO`, so it will not work until the carrier has assigned one \u2014 usually after pickup.\n\n`apiConfiguration.pickupOnly` is **false** here on purpose. Setting it to `true` means project44 *\"will not request a BOL for this shipment, and will only transmit pickup information to the carrier\"* \u2014 which would make `capacityProviderBolUrl`, the `BILL_OF_LADING` identifier and the whole Documents folder pointless. Turn it on only if you genuinely want a pickup with no bill of lading. It cannot be combined with `preScheduledPickup: true`.\n\n`noteConfiguration.noteSections` controls which sections project44 composes into the carrier-facing BOL note, in order.\n\n**`capacityProviderAccountGroup`**\n\n`code` names an account group configured in the project44 Self-Service Portal. If you omit it, the group named `Default` is used. Run *List Account Groups* in the Setup folder to see yours.\n\n`accounts` behaves differently per operation:\n\n| Operation | `accounts` |\n| --- | --- |\n| Rating, transit time | optional \u2014 empty or omitted means **all** accounts in the group |\n| Dispatch, cancellation, images | **exactly one** account is required |\n\nTwo identifiers are easy to confuse. `accounts[].code` is the **account-set code** you chose in the Self-Service Portal. The top-level `carrierCode` on a dispatch request is the carrier's **SCAC**. They are frequently the same string (`ABFS`, `SAIA`, `FXFE`), which hides the difference until the day it bites \u2014 *List Accounts in a Group* prints both side by side.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"capacityProviderAccountGroup\": {\n    \"code\": \"{{accountGroupCode}}\",\n    \"accounts\": [\n      {\n        \"code\": \"{{accountCode}}\"\n      }\n    ]\n  },\n  \"rateQuoteId\": \"{{rateQuoteId}}\",\n  \"capacityProviderQuoteNumber\": \"{{capacityProviderQuoteNumber}}\",\n  \"carrierCode\": \"{{carrierCode}}\",\n  \"originLocation\": {\n    \"address\": {\n      \"addressLines\": [\n        \"1200 Industrial Parkway\"\n      ],\n      \"city\": \"Barrington\",\n      \"state\": \"IL\",\n      \"postalCode\": \"60010\",\n      \"country\": \"US\"\n    },\n    \"contact\": {\n      \"companyName\": \"Northwind Manufacturing\",\n      \"contactName\": \"Jordan Avery\",\n      \"phoneNumber\": \"555-0100\",\n      \"email\": \"shipping@example.com\"\n    }\n  },\n  \"destinationLocation\": {\n    \"address\": {\n      \"addressLines\": [\n        \"4500 Commerce Drive\",\n        \"Dock 12\"\n      ],\n      \"city\": \"Bowling Green\",\n      \"state\": \"KY\",\n      \"postalCode\": \"42104\",\n      \"country\": \"US\"\n    },\n    \"contact\": {\n      \"companyName\": \"Southgate Distribution\",\n      \"contactName\": \"Riley Chen\",\n      \"phoneNumber\": \"555-0142\",\n      \"email\": \"receiving@example.com\"\n    }\n  },\n  \"requesterLocation\": {\n    \"address\": {\n      \"addressLines\": [\n        \"1200 Industrial Parkway\"\n      ],\n      \"city\": \"Barrington\",\n      \"state\": \"IL\",\n      \"postalCode\": \"60010\",\n      \"country\": \"US\"\n    },\n    \"contact\": {\n      \"companyName\": \"Northwind Manufacturing\",\n      \"contactName\": \"Jordan Avery\",\n      \"phoneNumber\": \"555-0100\",\n      \"email\": \"shipping@example.com\"\n    }\n  },\n  \"lineItems\": [\n    {\n      \"totalWeight\": 516,\n      \"packageDimensions\": {\n        \"length\": 48,\n        \"width\": 40,\n        \"height\": 43\n      },\n      \"freightClass\": \"92.5\",\n      \"packageType\": \"PLT\",\n      \"totalPackages\": 1,\n      \"totalPieces\": 11,\n      \"description\": \"ADHESIVES AND SEALANTS\",\n      \"stackable\": true,\n      \"nmfcItemCode\": \"46030\",\n      \"nmfcSubCode\": \"02\"\n    }\n  ],\n  \"totalHandlingUnits\": 1,\n  \"pickupWindow\": {\n    \"date\": \"{{pickupDate}}\",\n    \"startTime\": \"08:00\",\n    \"endTime\": \"15:30\"\n  },\n  \"deliveryWindow\": {\n    \"date\": \"{{deliveryDate}}\",\n    \"startTime\": \"08:00\",\n    \"endTime\": \"18:00\"\n  },\n  \"shipmentIdentifiers\": [\n    {\n      \"type\": \"BILL_OF_LADING\",\n      \"value\": \"BOL-{{$timestamp}}\"\n    },\n    {\n      \"type\": \"CUSTOMER_REFERENCE\",\n      \"value\": \"REF-{{$timestamp}}\"\n    }\n  ],\n  \"accessorialServices\": [],\n  \"pickupNote\": \"Dock opens at 08:00.\",\n  \"deliveryNote\": \"Appointment not required.\",\n  \"paymentTermsOverride\": \"PREPAID\",\n  \"currencyCode\": \"USD\",\n  \"weightUnit\": \"LB\",\n  \"lengthUnit\": \"IN\",\n  \"apiConfiguration\": {\n    \"noteConfiguration\": {\n      \"enableTruncation\": true,\n      \"noteSections\": [\n        {\n          \"name\": \"PICKUP_NOTE\"\n        },\n        {\n          \"name\": \"DELIVERY_NOTE\"\n        },\n        {\n          \"name\": \"PRIORITY_ACCESSORIALS\"\n        },\n        {\n          \"name\": \"PICKUP_ACCESSORIALS\"\n        },\n        {\n          \"name\": \"DELIVERY_ACCESSORIALS\"\n        },\n        {\n          \"name\": \"DIMENSIONS\"\n        }\n      ]\n    },\n    \"allowUnsupportedAccessorials\": false,\n    \"enableUnitConversion\": true,\n    \"fallBackToDefaultAccountGroup\": true,\n    \"pickupOnly\": false,\n    \"preScheduledPickup\": false\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201\", () => pm.response.to.have.status(201));",
                  "",
                  "const data = pm.response.json();",
                  "// Reset first so a failed or identifier-less dispatch never leaves an earlier",
                  "// shipment's identifiers for the cancellation requests to act on.",
                  "[\"shipmentId\", \"proNumber\", \"pickupNumber\", \"billOfLading\", \"bolUrl\", \"cancelIdValue\"]",
                  "    .forEach(k => pm.collectionVariables.set(k, \"\"));",
                  "if (pm.response.code !== 201) { return; }",
                  "if (data.id) { pm.collectionVariables.set(\"shipmentId\", data.id); }",
                  "",
                  "// Nearly all carriers return a PICKUP confirmation number; only a few return a PRO",
                  "// at dispatch time. Capture whichever came back and tell the cancellation request",
                  "// which identifier type to use.",
                  "const ids  = data.shipmentIdentifiers || [];",
                  "const find = t => (ids.find(i => i.type === t) || {}).value;",
                  "",
                  "const pro    = find(\"PRO\");",
                  "const pickup = find(\"PICKUP\");",
                  "const bol    = find(\"BILL_OF_LADING\");",
                  "",
                  "if (pro)    { pm.collectionVariables.set(\"proNumber\", pro); }",
                  "if (pickup) { pm.collectionVariables.set(\"pickupNumber\", pickup); }",
                  "if (bol)    { pm.collectionVariables.set(\"billOfLading\", bol); }",
                  "",
                  "if (pro) {",
                  "    pm.collectionVariables.set(\"cancelIdType\", \"PRO\");",
                  "    pm.collectionVariables.set(\"cancelIdValue\", pro);",
                  "} else if (pickup) {",
                  "    pm.collectionVariables.set(\"cancelIdType\", \"PICKUP\");",
                  "    pm.collectionVariables.set(\"cancelIdValue\", pickup);",
                  "}",
                  "",
                  "if (!pro) {",
                  "    console.log(\"No PRO returned -- only a few carriers issue one at dispatch.\");",
                  "    console.log(\"Query Shipment Images requires a PRO, so it will not work until one exists.\");",
                  "}",
                  "",
                  "// The carrier's BOL is the thing most people actually want back from a dispatch.",
                  "if (data.capacityProviderBolUrl) {",
                  "    pm.collectionVariables.set(\"bolUrl\", data.capacityProviderBolUrl);",
                  "    console.log(`BOL: ${data.capacityProviderBolUrl}`);",
                  "}",
                  "if (data.packingVisualizationUrl) {",
                  "    console.log(`Packing visualization: ${data.packingVisualizationUrl}`);",
                  "}",
                  "if (data.pickupDateTime) { console.log(`Pickup scheduled: ${data.pickupDateTime}`); }",
                  "",
                  "(data.infoMessages || []).forEach(m => console.log(`[${m.severity}] ${m.source}: ${m.message}`));",
                  "console.log(`shipmentId=${data.id} pro=${pro || \"(none)\"} pickup=${pickup || \"(none)\"}`);"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "4. Documents",
      "item": [
        {
          "name": "Query Shipment Images",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v4/ltl/images/query",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v4",
                "ltl",
                "images",
                "query"
              ]
            },
            "description": "Retrieves carrier documents for a dispatched shipment \u2014 BOL, delivery receipt, weight certificate and so on.\n\n`proNumber` and `pickupDate` identify the shipment at the carrier. `documentType` accepts values such as `BILL_OF_LADING`, `DELIVERY_RECEIPT` and `WEIGHT_CERTIFICATE`; `imageFormat` is typically `PDF`.\n\nThe `200` is a **single** `LtlImageReference` object \u2014 `{ documentType, imageFormat, imageUrl, proNumber, infoMessages }` \u2014 not a wrapper around an array. One request returns one document; ask for another `documentType` to get another.\n\nDocuments usually appear only once the carrier has picked up, so expect no `imageUrl` immediately after dispatch.\n\n**`capacityProviderAccountGroup`**\n\n`code` names an account group configured in the project44 Self-Service Portal. If you omit it, the group named `Default` is used. Run *List Account Groups* in the Setup folder to see yours.\n\n`accounts` behaves differently per operation:\n\n| Operation | `accounts` |\n| --- | --- |\n| Rating, transit time | optional \u2014 empty or omitted means **all** accounts in the group |\n| Dispatch, cancellation, images | **exactly one** account is required |\n\nTwo identifiers are easy to confuse. `accounts[].code` is the **account-set code** you chose in the Self-Service Portal. The top-level `carrierCode` on a dispatch request is the carrier's **SCAC**. They are frequently the same string (`ABFS`, `SAIA`, `FXFE`), which hides the difference until the day it bites \u2014 *List Accounts in a Group* prints both side by side.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"capacityProviderAccountGroup\": {\n    \"code\": \"{{accountGroupCode}}\",\n    \"accounts\": [\n      {\n        \"code\": \"{{accountCode}}\"\n      }\n    ]\n  },\n  \"proNumber\": \"{{proNumber}}\",\n  \"pickupDate\": \"{{pickupDate}}\",\n  \"documentType\": \"BILL_OF_LADING\",\n  \"imageFormat\": \"PDF\",\n  \"destinationAddress\": {\n    \"addressLines\": [\n      \"4500 Commerce Drive\"\n    ],\n    \"city\": \"Bowling Green\",\n    \"state\": \"KY\",\n    \"postalCode\": \"42104\",\n    \"country\": \"US\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "",
                  "// The 200 is a single LtlImageReference object -- not a wrapper around an array.",
                  "const img = pm.response.json();",
                  "",
                  "if (img.imageUrl) {",
                  "    pm.collectionVariables.set(\"imageUrl\", img.imageUrl);",
                  "    console.log(`${img.documentType} (${img.imageFormat}) for PRO ${img.proNumber}`);",
                  "    console.log(img.imageUrl);",
                  "} else {",
                  "    console.log(\"No image returned. Carriers usually publish documents only after pickup.\");",
                  "}",
                  "",
                  "(img.infoMessages || []).forEach(m => console.log(`[${m.severity}] ${m.source}: ${m.message}`));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "5. Cancellation",
      "item": [
        {
          "name": "Cancel by shipment identifiers",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v4/ltl/shipments/cancellations",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v4",
                "ltl",
                "shipments",
                "cancellations"
              ]
            },
            "description": "> \u26a0\ufe0f **Send only one of the two requests in this folder.** They are alternative ways to identify the same shipment, so running the folder straight through cancels via the first and then fails on the second, which is trying to cancel an already-cancelled shipment.\n\nCancels a dispatched shipment identified by carrier-assigned identifiers.\n\nThe identifier type is **not** hardcoded to `PRO`: the dispatch test sets `cancelIdType` and `cancelIdValue` to whatever the carrier actually returned, which is a `PICKUP` confirmation number far more often than a `PRO`.\n\n`shipmentIdentifiers` is **required** on this endpoint. If you want to cancel by the project44-generated shipment id instead, use *Cancel by project44 shipment id*; the two are mutually exclusive.\n\nReturns `200`.\n\n**`capacityProviderAccountGroup`**\n\n`code` names an account group configured in the project44 Self-Service Portal. If you omit it, the group named `Default` is used. Run *List Account Groups* in the Setup folder to see yours.\n\n`accounts` behaves differently per operation:\n\n| Operation | `accounts` |\n| --- | --- |\n| Rating, transit time | optional \u2014 empty or omitted means **all** accounts in the group |\n| Dispatch, cancellation, images | **exactly one** account is required |\n\nTwo identifiers are easy to confuse. `accounts[].code` is the **account-set code** you chose in the Self-Service Portal. The top-level `carrierCode` on a dispatch request is the carrier's **SCAC**. They are frequently the same string (`ABFS`, `SAIA`, `FXFE`), which hides the difference until the day it bites \u2014 *List Accounts in a Group* prints both side by side.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"capacityProviderAccountGroup\": {\n    \"code\": \"{{accountGroupCode}}\",\n    \"accounts\": [\n      {\n        \"code\": \"{{accountCode}}\"\n      }\n    ]\n  },\n  \"originAddress\": {\n    \"addressLines\": [\n      \"1200 Industrial Parkway\"\n    ],\n    \"city\": \"Barrington\",\n    \"state\": \"IL\",\n    \"postalCode\": \"60010\",\n    \"country\": \"US\"\n  },\n  \"shipmentIdentifiers\": [\n    {\n      \"type\": \"{{cancelIdType}}\",\n      \"value\": \"{{cancelIdValue}}\"\n    }\n  ],\n  \"apiConfiguration\": {\n    \"fallBackToDefaultAccountGroup\": true\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Cancellation accepted\", () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "(pm.response.json().infoMessages || []).forEach(m => console.log(`[${m.severity}] ${m.message}`));"
                ]
              }
            }
          ]
        },
        {
          "name": "Cancel by project44 shipment id",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v4/ltl/shipments/{{shipmentId}}/cancellations",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v4",
                "ltl",
                "shipments",
                "{{shipmentId}}",
                "cancellations"
              ]
            },
            "description": "> \u26a0\ufe0f **Send only one of the two requests in this folder.** They are alternative ways to identify the same shipment, so running the folder straight through cancels via the first and then fails on the second, which is trying to cancel an already-cancelled shipment.\n\nCancels using the project44-generated shipment id captured by the dispatch request.\n\n**Do not send `shipmentIdentifiers` on this endpoint.** Supplying it returns `400` with *\"No shipment identifiers may be provided when cancelling by project44-generated shipment id.\"* \u2014 the body above deliberately omits it.\n\nReturns `200`.\n\n**`capacityProviderAccountGroup`**\n\n`code` names an account group configured in the project44 Self-Service Portal. If you omit it, the group named `Default` is used. Run *List Account Groups* in the Setup folder to see yours.\n\n`accounts` behaves differently per operation:\n\n| Operation | `accounts` |\n| --- | --- |\n| Rating, transit time | optional \u2014 empty or omitted means **all** accounts in the group |\n| Dispatch, cancellation, images | **exactly one** account is required |\n\nTwo identifiers are easy to confuse. `accounts[].code` is the **account-set code** you chose in the Self-Service Portal. The top-level `carrierCode` on a dispatch request is the carrier's **SCAC**. They are frequently the same string (`ABFS`, `SAIA`, `FXFE`), which hides the difference until the day it bites \u2014 *List Accounts in a Group* prints both side by side.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"capacityProviderAccountGroup\": {\n    \"code\": \"{{accountGroupCode}}\",\n    \"accounts\": [\n      {\n        \"code\": \"{{accountCode}}\"\n      }\n    ]\n  },\n  \"originAddress\": {\n    \"addressLines\": [\n      \"1200 Industrial Parkway\"\n    ],\n    \"city\": \"Barrington\",\n    \"state\": \"IL\",\n    \"postalCode\": \"60010\",\n    \"country\": \"US\"\n  },\n  \"apiConfiguration\": {\n    \"fallBackToDefaultAccountGroup\": true\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Cancellation accepted\", () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));",
                  "(pm.response.json().infoMessages || []).forEach(m => console.log(`[${m.severity}] ${m.message}`));"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
