# Shipping

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## POST /shipping/rates

**Get shipping rates**

`operationId: ShippingController_getShippingRates`

Quotes rates from the configured carriers for a set of parcels travelling between two addresses. This is the first step of the direct flow: quote, choose, then buy a label.

Pass the chosen rate object back to `POST /shipping/create` unchanged — carriers key their label purchase on the rate id, so a reconstructed object will not work.

#### Signature

```http
POST /shipping/rates (body) -> Available rates across configured carriers
```

#### Access

Public — no credentials required.

#### Notes

- Rates expire. Buy the label promptly, or re-quote before purchasing.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | PROVIDER_NOT_CONFIGURED | Shipping integration not configured. Please configure EasyPost or another shipping provider. | Neither the org nor the shared org has an active config whose `useCases` include `Shipping`. When the shared org is reached but has none either, the response is instead a `404` "Integration with useCase Shipping not found in org <shared org>". | Configure a carrier integration — `GET /shipping/admin/providers` lists what is supported. Using the platform's shared carrier needs the org to accept its service agreement (a `402` otherwise). |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/create`
- `POST /shipping/order/rates`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

What is being shipped, and between where.

```json
{
  "fromAddress": {
    "name": "Acme",
    "street1": "4 Warehouse Way",
    "city": "Newark",
    "state": "NJ",
    "zip": "07102",
    "country": "US"
  },
  "toAddress": {
    "name": "Ada Lovelace",
    "street1": "12 Ada Way",
    "city": "Boston",
    "state": "MA",
    "zip": "02108",
    "country": "US"
  },
  "parcels": [
    {
      "length": 12,
      "width": 9,
      "height": 4,
      "weight": 2.5
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Available rates across configured carriers |
| `400` | Shipping integration not configured. Please configure EasyPost or another shipping provider. — Neither the org nor the shared org has an active config whose `useCases` include `Shipping`. When the shared org is reached but has none either, the response is instead a `404` "Integration with useCase Shipping not found in org <shared org>". |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/create

**Create a shipping label**

`operationId: ShippingController_createShipping`

Buys the label for a chosen rate and creates the shipping record. **This spends money** — the carrier charges for the label at this point.

Not idempotent: a retried request buys a second label. If a response is lost, look the shipment up with `GET /shipping/list` before trying again.

#### Signature

```http
POST /shipping/create (body) -> The shipping record, including the label and tracking number
```

#### Access

Public — no credentials required.

#### Notes

- Buys a real label and incurs a real charge. Treat it as non-repeatable.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | PROVIDER_NOT_CONFIGURED | Shipping integration not configured. Please configure EasyPost or another shipping provider. | Neither the org nor the shared org has an active config whose `useCases` include `Shipping`. When the shared org is reached but has none either, the response is instead a `404` "Integration with useCase Shipping not found in org <shared org>". | Configure a carrier integration — `GET /shipping/admin/providers` lists what is supported. Using the platform's shared carrier needs the org to accept its service agreement (a `402` otherwise). |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/rates`
- `POST /shipping/cancel`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The chosen rate and the shipment details.

```json
{
  "orderNumber": "A7K2M9QX4",
  "rate": {
    "id": "rate_abc123",
    "carrier": "USPS",
    "service": "Priority",
    "rate": "8.45"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The shipping record, including the label and tracking number |
| `400` | Shipping integration not configured. Please configure EasyPost or another shipping provider. — Neither the org nor the shared org has an active config whose `useCases` include `Shipping`. When the shared org is reached but has none either, the response is instead a `404` "Integration with useCase Shipping not found in org <shared org>". |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/refresh-tracking

**Refresh tracking and apply any change**

`operationId: ShippingController_refreshTracking`

Asks the carrier where a parcel is right now, then applies **the same rules as the webhook**: the order is updated and the customer emailed only when the carrier status differs from the one already processed.

That makes it safe to click repeatedly — an unchanged status costs one carrier lookup and does nothing else. No duplicate emails, no redundant writes.

Pass `orderNumber` to check every shipment on an order, or `trackingNumber` for one. Set `apply: false` to preview without writing anything, or `notify: false` to update the order without emailing the customer.

#### Signature

```http
POST /shipping/refresh-tracking (body) -> What changed, per shipment
```

#### Access

Public — no credentials required.

#### Notes

- Idempotent by design — repeated calls with an unchanged carrier status have no side effects.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CARRIER_ERROR | The carrier rejected the request. | The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. | The carrier message is passed through. Verify the address first with `POST /shipping/verify-address`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/tracking-webhook`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `trackingNumber` | path | any | yes | Tracking number |

### Request body

What to check, and what to do about it.

```json
{
  "orderNumber": "A7K2M9QX4"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | What changed, per shipment |
| `400` | The carrier rejected the request. — The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /shipping/tracking-webhook

**Read saved automatic tracking setup without contacting the provider**

`operationId: ShippingController_getTrackingWebhookSetup`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization ID |

### Responses

| Status | Meaning |
| --- | --- |
| `200` |  |

## POST /shipping/tracking-webhook

**Register the tracking webhook**

`operationId: ShippingController_ensureTrackingWebhook`

Points the carrier integration at this org's webhook so carrier scans arrive automatically and shipping status updates without anyone polling.

**Idempotent** — registrations are matched on URL, so calling it repeatedly is safe.

When the provider is not configured it returns `{ ready: false, url }` rather than throwing, so setup flows can check readiness without handling an error.

#### Signature

```http
POST /shipping/tracking-webhook () -> Whether the webhook is registered, and at what URL
```

#### Access

Public — no credentials required.

#### Notes

- Never throws for a missing provider — check `ready` in the response.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/refresh-tracking`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Whether the webhook is registered, and at what URL |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "ready": true,
  "url": "https://api.appmint.io/shipping/webhook/acme-retail"
}
```

## GET /shipping/track/{trackingNumber}

**Track a shipment**

`operationId: ShippingController_trackShipment`

Returns the carrier's current tracking information for a tracking number. A read-only lookup — it does not update the order or notify anyone.

Use `POST /shipping/refresh-tracking` when you want a status change to actually propagate.

#### Signature

```http
GET /shipping/track/{trackingNumber} (trackingNumber: string) -> Tracking information from the carrier
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SHIPPING_NOT_FOUND | Shipping record not found | No shipping record matches the id, tracking number or order number given. | List records with `GET /shipping/list`. |
| `400` | CARRIER_ERROR | The carrier rejected the request. | The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. | The carrier message is passed through. Verify the address first with `POST /shipping/verify-address`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/refresh-tracking`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `trackingNumber` | path | string | yes | Carrier tracking number. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Tracking information from the carrier |
| `400` | The carrier rejected the request. — The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. |
| `404` | Shipping record not found — No shipping record matches the id, tracking number or order number given. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /shipping/google-keys

**Get the Google Maps key for this org**

`operationId: ShippingController_getGoogleKeys`

Returns the Maps API key a client should use, and says where it came from.

When the org has configured its own Google provider with a `mapsApiKey`, that key is returned with `source: "org"` and `billable: false` — they pay Google directly and nothing is metered here.

Otherwise the shared platform key is returned with `source: "shared"` and `billable: true`, and the org's agreement and balance are checked **before** the key is handed over. An org without an agreement or in arrears does not get a key.

#### Signature

```http
GET /shipping/google-keys () -> The key and its billing provenance
```

#### Access

Public — no credentials required.

#### Notes

- A `billable: true` response means the org is being charged for Maps usage — surface that in any UI that spends it heavily.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/address-autocomplete`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Autocomplete request

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The key and its billing provenance |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "mapsApiKey": "AIza...",
  "source": "shared",
  "billable": true
}
```

## POST /shipping/address-autocomplete

**Autocomplete an address**

`operationId: ShippingController_getAddressAutocomplete`

Returns address predictions as the customer types, backed by Google Places. Defaults to US and Canada; pass `components` to widen or narrow that.

Send a `sessiontoken` and keep it constant across the keystrokes of one lookup, then reuse it for the matching `place-details` call — Google bills an autocomplete session as a unit, and omitting the token makes every keystroke a separate billable request.

#### Signature

```http
POST /shipping/address-autocomplete (body) -> Address predictions
```

#### Access

Public — no credentials required.

#### Notes

- May be billed against the shared platform Google key — see `GET /shipping/google-keys`.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /shipping/place-details/{placeId}`
- `GET /shipping/google-keys`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

What the customer has typed so far.

```json
{
  "input": "12 Ada W",
  "sessiontoken": "b1f2c3d4-e5f6",
  "components": "country:us|country:ca"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Address predictions |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /shipping/place-details/{placeId}

**Get address details for a place**

`operationId: ShippingController_getPlaceDetails`

Expands a place id from autocomplete into a full structured address ready to use as a shipping address.

#### Signature

```http
GET /shipping/place-details/{placeId} (placeId: string) -> The structured address
```

#### Access

Public — no credentials required.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/address-autocomplete`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `placeId` | path | string | yes | Google Place ID from an autocomplete prediction. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The structured address |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/verify-address

**Verify a shipping address**

`operationId: ShippingController_verifyShippingAddress`

Validates and normalises an address with the carrier, correcting formatting and flagging anything undeliverable. Worth calling before buying a label — a bad address is the most common cause of a rejected shipment.

#### Signature

```http
POST /shipping/verify-address (body) -> The verified and normalised address
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | CARRIER_ERROR | The carrier rejected the request. | The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. | The carrier message is passed through. Verify the address first with `POST /shipping/verify-address`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/address-autocomplete`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The address to verify.

```json
{
  "name": "Ada Lovelace",
  "street1": "12 Ada Way",
  "city": "Boston",
  "state": "MA",
  "zip": "02108",
  "country": "US"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The verified and normalised address |
| `400` | The carrier rejected the request. — The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /shipping/methods

**Get shipping methods**

`operationId: ShippingController_getShippingMethods`

The shipping methods and carriers available to this org. Use it to build a delivery-option selector rather than hard-coding carriers.

#### Signature

```http
GET /shipping/methods () -> Available methods and carriers
```

#### Access

Public — no credentials required.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /shipping/admin/configs`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Available methods and carriers |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /shipping/list

**List shipping records**

`operationId: ShippingController_listShippings`

Lists shipments with optional filters and paging.

#### Signature

```http
GET /shipping/list (orderNumber?: string, status?: string, author?: string, page?: integer, pageSize?: integer) -> A page of shipping records
```

#### Access

Public — no credentials required.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /shipping/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | query | string | — |  |
| `status` | query | string | — |  |
| `author` | query | string | — | Filter by the customer or operator on the record. |
| `page` | query | integer | — |  |
| `pageSize` | query | integer | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | A page of shipping records |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/cancel

**Cancel a shipment**

`operationId: ShippingController_cancelShipping`

Cancels a shipment and requests a refund for the label from the carrier. Identify it by shipping id, tracking number or order number.

Carriers only refund labels that have not entered their network, and refunds are typically processed on their own schedule rather than immediately.

#### Signature

```http
POST /shipping/cancel (body) -> The cancellation result
```

#### Access

Public — no credentials required.

#### Notes

- A successful cancellation is a refund *request*. Whether the carrier honours it is their decision.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SHIPPING_NOT_FOUND | Shipping record not found | No shipping record matches the id, tracking number or order number given. | List records with `GET /shipping/list`. |
| `400` | CARRIER_ERROR | The carrier rejected the request. | The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. | The carrier message is passed through. Verify the address first with `POST /shipping/verify-address`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/create`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Which shipment to cancel. Send at least one identifier.

```json
{
  "shippingId": "66f1a2b3c4d5e6f708192a3b"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The cancellation result |
| `400` | The carrier rejected the request. — The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. |
| `404` | Shipping record not found — No shipping record matches the id, tracking number or order number given. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/update-address

**Update a delivery address**

`operationId: ShippingController_updateShippingAddress`

Changes the delivery address on a shipment. **Only possible before the label is purchased** — once a carrier has the shipment, the address is fixed and the shipment must be cancelled and recreated.

#### Signature

```http
POST /shipping/update-address (body) -> The updated shipping record
```

#### Access

Public — no credentials required.

#### Notes

- Cancel and recreate the shipment if the label has already been bought.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SHIPPING_NOT_FOUND | Shipping record not found | No shipping record matches the id, tracking number or order number given. | List records with `GET /shipping/list`. |
| `400` | CARRIER_ERROR | The carrier rejected the request. | The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. | The carrier message is passed through. Verify the address first with `POST /shipping/verify-address`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/cancel`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The shipment and its new address.

```json
{
  "shippingId": "66f1a2b3c4d5e6f708192a3b",
  "address": {
    "name": "Ada Lovelace",
    "street1": "12 Ada Way",
    "city": "Boston",
    "state": "MA",
    "zip": "02108",
    "country": "US"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated shipping record |
| `400` | The carrier rejected the request. — The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. |
| `404` | Shipping record not found — No shipping record matches the id, tracking number or order number given. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/estimated-delivery

**Get estimated delivery times**

`operationId: ShippingController_getEstimatedDelivery`

Returns estimated transit times per carrier and service for a route, without quoting prices or creating anything. Use it to show "arrives Tuesday" on a delivery-option selector.

Narrow the result with `carrier` or `service` when you only care about one option.

#### Signature

```http
POST /shipping/estimated-delivery (body) -> Transit-time estimates per carrier and service
```

#### Access

Public — no credentials required.

#### Notes

- Estimates are the carrier's, not a guarantee, and exclude your own handling time before dispatch.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | PROVIDER_NOT_CONFIGURED | Shipping integration not configured. Please configure EasyPost or another shipping provider. | Neither the org nor the shared org has an active config whose `useCases` include `Shipping`. When the shared org is reached but has none either, the response is instead a `404` "Integration with useCase Shipping not found in org <shared org>". | Configure a carrier integration — `GET /shipping/admin/providers` lists what is supported. Using the platform's shared carrier needs the org to accept its service agreement (a `402` otherwise). |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/rates`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The route and parcels to estimate for.

```json
{
  "fromAddress": {
    "name": "Acme",
    "street1": "4 Warehouse Way",
    "city": "Newark",
    "state": "NJ",
    "zip": "07102",
    "country": "US"
  },
  "toAddress": {
    "name": "Ada Lovelace",
    "street1": "12 Ada Way",
    "city": "Boston",
    "state": "MA",
    "zip": "02108",
    "country": "US"
  },
  "parcels": [
    {
      "length": 12,
      "width": 9,
      "height": 4,
      "weight": 2.5
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Transit-time estimates per carrier and service |
| `400` | Shipping integration not configured. Please configure EasyPost or another shipping provider. — Neither the org nor the shared org has an active config whose `useCases` include `Shipping`. When the shared org is reached but has none either, the response is instead a `404` "Integration with useCase Shipping not found in org <shared org>". |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /shipping/label/{shippingId}/{format}

**Get a shipping label by id**

`operationId: ShippingController_getShippingLabelById`

The URL form of the label retrieval, convenient for linking to directly from an operator UI.

#### Signature

```http
GET /shipping/label/{shippingId}/{format} (shippingId: string, format: string) -> The label
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SHIPPING_NOT_FOUND | Shipping record not found | No shipping record matches the id, tracking number or order number given. | List records with `GET /shipping/list`. |
| `400` | CARRIER_ERROR | The carrier rejected the request. | The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. | The carrier message is passed through. Verify the address first with `POST /shipping/verify-address`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/label`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `shippingId` | path | string | yes | Shipping record id. |
| `format` | query | "PDF" \| "PNG" \| "ZPL" \| "EPL2" | — | Label format |
| `format` | path | string | yes | Label format — one of `PDF`, `PNG`, `ZPL`, `EPL2`. Defaults to PDF. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The label |
| `400` | The carrier rejected the request. — The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. |
| `404` | Shipping record not found — No shipping record matches the id, tracking number or order number given. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/label

**Get a shipping label**

`operationId: ShippingController_getShippingLabel`

Retrieves the label for an existing shipment in a chosen format. Identify it by shipping record id, provider shipment id, or tracking number.

#### Signature

```http
POST /shipping/label (body) -> The label
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SHIPPING_NOT_FOUND | Shipping record not found | No shipping record matches the id, tracking number or order number given. | List records with `GET /shipping/list`. |
| `400` | CARRIER_ERROR | The carrier rejected the request. | The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. | The carrier message is passed through. Verify the address first with `POST /shipping/verify-address`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /shipping/label/{shippingId}/{format}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Which shipment, and in what format.

```json
{
  "shippingId": "66f1a2b3c4d5e6f708192a3b",
  "format": "PDF"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The label |
| `400` | The carrier rejected the request. — The carrier API returned an error — an undeliverable address, a parcel outside its limits, or missing credentials. |
| `404` | Shipping record not found — No shipping record matches the id, tracking number or order number given. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /shipping/{id}

**Get a shipping record**

`operationId: ShippingController_getShipping`

Fetches one shipment by record id **or** order number.

This route is declared last among the single-segment GETs, so `/shipping/list` and `/shipping/methods` resolve as their own endpoints rather than being read as ids.

#### Signature

```http
GET /shipping/{id} (id: string) -> The shipping record
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SHIPPING_NOT_FOUND | Shipping record not found | No shipping record matches the id, tracking number or order number given. | List records with `GET /shipping/list`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /shipping/order/shipments/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Shipping record id or order number. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The shipping record |
| `404` | Shipping record not found — No shipping record matches the id, tracking number or order number given. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /shipping/locations/available

**Get available ship-from locations**

`operationId: ShippingController_getAvailableLocations`

Locations that can be used as an origin address. Where an org has exactly one, the order endpoints select it automatically and `fromLocationId` can be omitted.

#### Signature

```http
GET /shipping/locations/available () -> Locations usable as a shipping origin
```

#### Access

Public — no credentials required.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/order/rates`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Locations usable as a shipping origin |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /shipping/order/shipments/{orderNumber}

**Get shipments for an order**

`operationId: ShippingController_getShipmentsForOrder`

Every shipment raised against an order. An order shipped in parts has several, one per parcel or per fulfilment run.

#### Signature

```http
GET /shipping/order/shipments/{orderNumber} (orderNumber: string) -> The order's shipments
```

#### Access

Public — no credentials required.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /shipping/order/status/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | Public order number. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The order's shipments |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /shipping/order/status/{orderNumber}

**Get shipping status for an order**

`operationId: ShippingController_getOrderShippingStatus`

The fulfilment picture for an order: which lines have shipped and which are still outstanding.

This is the read that makes partial shipments manageable — it answers "what is left to send" without reconciling shipments against order lines yourself.

#### Signature

```http
GET /shipping/order/status/{orderNumber} (orderNumber: string) -> Shipped and pending lines for the order
```

#### Access

Public — no credentials required.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/order/rates`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | Public order number. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Shipped and pending lines for the order |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/order/rates

**Get shipping rates for an order**

`operationId: ShippingController_getShippingRatesForOrder`

Quotes rates for an order, pulling the destination and the items from the order itself.

For a partial shipment, name the lines in `productSkus`; omit it to quote for everything outstanding. `fromLocationId` can be omitted when the org has only one location.

#### Signature

```http
POST /shipping/order/rates (body) -> Available rates for the order
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | PROVIDER_NOT_CONFIGURED | Shipping integration not configured. Please configure EasyPost or another shipping provider. | Neither the org nor the shared org has an active config whose `useCases` include `Shipping`. When the shared org is reached but has none either, the response is instead a `404` "Integration with useCase Shipping not found in org <shared org>". | Configure a carrier integration — `GET /shipping/admin/providers` lists what is supported. Using the platform's shared carrier needs the org to accept its service agreement (a `402` otherwise). |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/order/create`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The order, and optionally which lines to ship.

```json
{
  "orderNumber": "A7K2M9QX4"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Available rates for the order |
| `400` | Shipping integration not configured. Please configure EasyPost or another shipping provider. — Neither the org nor the shared org has an active config whose `useCases` include `Shipping`. When the shared org is reached but has none either, the response is instead a `404` "Integration with useCase Shipping not found in org <shared org>". |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/order/ship

**Create shipment for order**

`operationId: ShippingController_shipOrder`

Create a shipment for an order. Supports partial shipments by specifying specific products.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization ID |

### Request body

Shipment creation request

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Returns created shipment with label |

## POST /shipping/order/create

**Create a shipment for an order**

`operationId: ShippingController_createShippingForOrder`

Buys a label for an order and records the shipment against it. **This spends money.**

Supports partial shipments through `productSkus` — ship what is in stock now and the rest later, and the order accumulates one shipment per dispatch.

Pass the `rate` object from `POST /shipping/order/rates` unchanged.

#### Signature

```http
POST /shipping/order/create (body) -> The created shipment with its label
```

#### Access

Public — no credentials required.

#### Notes

- Buys a real label. Not idempotent — check `GET /shipping/order/shipments/{orderNumber}` before retrying.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | PROVIDER_NOT_CONFIGURED | Shipping integration not configured. Please configure EasyPost or another shipping provider. | Neither the org nor the shared org has an active config whose `useCases` include `Shipping`. When the shared org is reached but has none either, the response is instead a `404` "Integration with useCase Shipping not found in org <shared org>". | Configure a carrier integration — `GET /shipping/admin/providers` lists what is supported. Using the platform's shared carrier needs the org to accept its service agreement (a `402` otherwise). |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/order/rates`
- `POST /shipping/order/manual`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The order, the chosen rate, and optionally which lines.

```json
{
  "orderNumber": "A7K2M9QX4",
  "rate": {
    "id": "rate_abc123",
    "carrier": "USPS",
    "service": "Priority",
    "rate": "8.45"
  },
  "productSkus": [
    "DRK-COLA-330"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created shipment with its label |
| `400` | Shipping integration not configured. Please configure EasyPost or another shipping provider. — Neither the org nor the shared org has an active config whose `useCases` include `Shipping`. When the shared org is reached but has none either, the response is instead a `404` "Integration with useCase Shipping not found in org <shared org>". |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/order/manual

**Add manual shipping info to an order**

`operationId: ShippingController_addManualShipping`

Records shipping details for a parcel sent **outside** the carrier integration — a label bought at a post office counter, a courier booked directly, a local delivery driver. No carrier account is involved and no label is purchased.

A tracking URL is generated automatically for carriers the platform recognises; pass `trackingUrl` for anything else.

The customer is emailed unless you pass `sendNotification: false`.

#### Signature

```http
POST /shipping/order/manual (body) -> The updated order with its shipping info
```

#### Access

Public — no credentials required.

#### Notes

- No carrier integration is needed, so this works even when no provider is configured.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/order/create`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The order and the shipping details to record.

```json
{
  "orderNumber": "A7K2M9QX4",
  "shippingInfo": {
    "carrier": "FedEx",
    "tracker": "794644790134",
    "rate": "Ground",
    "cost": "11.20"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated order with its shipping info |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/calculate

**Calculate cart shipping cost**

`operationId: ShippingController_calculateCartShipping`

Computes shipping for a cart from the org's shipping configuration — flat, weight-banded, zone-based or live carrier rates, whichever the config specifies — including any free-shipping threshold.

#### Signature

```http
POST /shipping/calculate (body) -> The calculated shipping cost
```

#### Access

Public — no credentials required.

#### Notes

- Pass `orderTotal` or a free-shipping threshold cannot be evaluated and the customer will be charged.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/product-cost`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The cart and destination.

```json
{
  "items": [
    {
      "sku": "DRK-COLA-330",
      "quantity": 2
    }
  ],
  "toAddress": {
    "city": "Boston",
    "state": "MA",
    "zip": "02108",
    "country": "US"
  },
  "orderTotal": 129.99
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The calculated shipping cost |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/options

**Shipping options for a cart and destination**

`operationId: ShippingController_getShippingOptions`

Every way this cart can be shipped to this address, cheapest first, following the shipping rules: a configuration scoped to the destination (country / state / postcode) beats an unscoped one, specificity then priority break ties, and per-product rules come first. Carrier configurations return one option per service.

When items need different configurations the cart is split: each option's `amount` is the whole order's shipping and `groups` says how every part ships. Products priced "free" or "flat" on the product itself are folded in (`breakdown.forced`).

`served: false` means no configuration ships there — `problems` and `message` say why; the answer is honest, not a guessed rate. A free-shipping promotion (automatic, or via `discountCode`) is applied after pricing: it frees the cheapest option, or every option when the promotion is set to `any`, marking them `freeShipping` with a `freeReason`. How to present the list is the client's call.

#### Signature

```http
POST /shipping/options (body) -> { served, options[], currency, problems?, message? }
```

#### Access

Public — no credentials required.

#### Notes

- Nothing configured at all returns `served: false` with "Shipping has not been set up yet — add a shipping configuration to quote rates".

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/calculate`
- `POST /shipping/product-cost`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "items": [
    {
      "sku": "DRK-COLA-330",
      "quantity": 2
    }
  ],
  "toAddress": {
    "street1": "1 Main St",
    "city": "Boston",
    "state": "MA",
    "zip": "02108",
    "country": "US"
  },
  "orderTotal": 42
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { served, options[], currency, problems?, message? } |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /shipping/product-cost

**Get shipping cost for one product**

`operationId: ShippingController_getProductShippingCost`

Shipping cost for a single product, for display on a product page ("+ $4.99 shipping"). Falls back to a US destination and the org's own location when addresses are omitted, so it can be called before a customer has entered anything.

#### Signature

```http
POST /shipping/product-cost (body) -> The product's shipping cost
```

#### Access

Public — no credentials required.

#### Notes

- The default destination makes the figure indicative only — recompute at checkout with the real address.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /shipping/calculate`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The product and, optionally, where it is going.

```json
{
  "sku": "DRK-COLA-330",
  "quantity": 1
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The product's shipping cost |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

