(Beta) Ship packages with Shippo. Multi-carrier rate shopping, label generation, package tracking, address validation, customs declarations, and batch proces...
---
name: goshippo
description: "(Beta) Ship packages with Shippo. Multi-carrier rate shopping, label generation, package tracking, address validation, customs declarations, and batch processing from CSV files."
version: 1.1.3
metadata:
openclaw:
requires:
env:
- SHIPPO_API_KEY
bins:
# Node.js is required for the primary (self-host @shippo/shippo-mcp via npx) path,
# and for the mcp-remote fallback for MCP clients that don't speak type:http with
# custom headers. The pure Gram HTTPS path needs no local Node, but we list node
# here because both documented options end up needing it for most clients.
- node
primaryEnv: SHIPPO_API_KEY
envVars:
- name: SHIPPO_API_KEY
required: true
description: "Your Shippo API key value (just the token β no 'ShippoToken' prefix; that's added by the MCP config). Format: 'shippo_test_xxxxx' (test mode, free) or 'shippo_live_xxxxx' (live mode, real charges). Get yours at https://apps.goshippo.com/settings/api."
install:
kind: node
package: "@shippo/shippo-mcp"
emoji: "π¦"
homepage: https://github.com/goshippo/ai
---
# Shippo Shipping Skill
## Setup
> ### β οΈ Breaking change in v1.1.0 β set `SHIPPO_API_KEY` to the bare token (no `ShippoToken` prefix)
>
> Previous docs said the env var should be `ShippoToken shippo_test_xxx`. That format double-prefixed the auth header (the args/headers block adds `ShippoToken ` automatically) and would produce auth failures.
>
> **Update your env var to just the token value** (no `ShippoToken ` prefix; the MCP config in the examples below prepends it for you). The value should look like `<your-shippo-key>` β a token starting with `shippo_test_` (sandbox) or `shippo_live_` (production). Grab one from the [Shippo dashboard](https://apps.goshippo.com/settings/api).
**MCP server (default):** Shippo-hosted via [Gram](https://app.getgram.ai) (Speakeasy-operated MCP gateway) at `https://app.getgram.ai/mcp/shippo-key-auth`. The MCP client connects over HTTPS with your Shippo API key passed as a custom header β no local Node process or npm install required.
Configure your MCP client with:
```json
{
"mcpServers": {
"shippo": {
"type": "http",
"url": "https://app.getgram.ai/mcp/shippo-key-auth",
"headers": {
"Mcp-Shippo-Key-Auth-Api-Key-Header": "ShippoToken ${SHIPPO_API_KEY}"
}
}
}
}
```
For MCP clients that don't support `type: http` with custom headers (older Claude Desktop / Cursor versions), use the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) stdio-to-HTTP bridge to the same Gram URL (requires Node.js 18+):
```json
{
"mcpServers": {
"shippo": {
"command": "npx",
"args": [
"-y",
"mcp-remote@latest",
"https://app.getgram.ai/mcp/shippo-key-auth",
"--header",
"Mcp-Shippo-Key-Auth-Api-Key-Header:ShippoToken ${SHIPPO_API_KEY}"
]
}
}
}
```
### Alternative: self-host with the npm package (no third-party gateway)
If you'd rather not route through Gram, the [`@shippo/shippo-mcp`](https://www.npmjs.com/package/@shippo/shippo-mcp) npm package runs as a local stdio MCP server and talks directly to `api.goshippo.com`. Same auth model, no third-party gateway in the path. Requires Node.js 18+.
```json
{
"mcpServers": {
"shippo": {
"command": "npx",
"args": [
"-y",
"@shippo/shippo-mcp",
"start",
"--api-key-header",
"ShippoToken ${SHIPPO_API_KEY}",
"--shippo-api-version",
"2018-02-08"
]
}
}
}
```
If your MCP client does not interpolate `${SHIPPO_API_KEY}` inside `args[]`, substitute the literal `ShippoToken shippo_{test|live}_xxxxx` value into the `--api-key-header` string.
**Prerequisites:** A valid Shippo API key and at least one carrier account (Shippo provides managed accounts for USPS, UPS, FedEx, DHL Express by default). See `references/tool-reference.md` for the full tool catalog.
**Test vs live mode** -- check the API key prefix before any purchase workflow:
- **`shippo_test_*`**: Labels are free. No real charges. Tracking uses mock numbers only.
- **`shippo_live_*`**: Real charges. Inform the user which mode they are in.
Test and live mode have completely separate data and object IDs.
**Response envelope:** The MCP wraps most API responses in a Speakeasy envelope shaped like `{"ContentType": "application/json", "StatusCode": <code>, "RawResponse": {}, "<PayloadName>": {...actual response...}}`. The payload field is named after the response schema on success (e.g. `ParsedAddress`, `AddressPaginatedList`, `AddressValidationResultV2`, `AddressWithMetadataResponse`, `Shipment`, `CarrierAccountPaginatedList`) and after the HTTP status code on some errors (e.g. `fourHundredAndNineApplicationJsonObject` for a 409 β the body may be `{}`). To extract the payload, find the field whose key is not `ContentType`, `StatusCode`, or `RawResponse`, and branch on `StatusCode` for success vs error.
**Non-envelope errors:** Some failures bypass the envelope entirely and surface as an MCP-level error instead β the tool response has `isError: true` with a single text block containing a plaintext message like `Unexpected API response status or content-type: Status 404 Content-Type application/json Body: {"detail":"Not found."}`. Argument-validation failures come back as JSON-RPC error code `-32602`. Handle both paths when reporting errors to the user.
---
## Best Practices
Latest Shippo API version: **2018-02-08**. Send via the `Shippo-API-Version` header.
### Integration routing
| Building⦠| Recommended primitive | See |
|----------------------------------------------------|--------------------------------|--------------------------------------------------------------------------------------------|
| Checkout flow with live shipping rates | Rates at Checkout | Rate Shopping (+ `shippo/references/rate-shopping-guide.md`) |
| Single label purchase | Shipments + Transactions | Label Purchase |
| Bulk label generation from CSV | Batches + Manifests | Batch Shipping (+ `shippo/references/csv-format.md`) |
| Track packages across carriers | Tracking + webhooks | Tracking (+ `shippo/references/test-mode.md`) |
| Validate user addresses before save | Addresses v2 | Address Validation (+ `shippo/references/address-formats.md`) |
| Analyze shipping spend / optimize carriers | Shipments + Transactions list | Shipping Analysis |
| International shipments | Customs Items + Declarations | Label Purchase (+ `shippo/references/customs-guide.md` + `shippo/references/international-shipping.md`) |
Read the relevant skill or reference before answering integration questions or writing code.
### Critical rules
- **Always validate addresses before purchasing labels.** Most "no rates" / "label failed" errors trace back to unvalidated addresses.
- **Test mode (`shippo_test_*`) is free; live mode (`shippo_live_*`) charges real money.** Always inform the user which mode an API key is in before any purchase workflow.
- **Always confirm purchase before `purchase_shipping_label`.** Show carrier/service/cost/eta and require explicit user confirmation.
- **Parcel dimensions and weight must be strings, not numbers.** Use `"10"`, never `10`.
- **Label URLs are S3 signed URLs.** Always display the complete URL β truncating breaks the signature.
- **Rates expire after 7 days.** Re-create the shipment for fresh rates.
### Response handling
The MCP wraps responses in a Speakeasy envelope. Some failures bypass the envelope. See `shippo/references/response-envelope.md` and `shippo/references/error-reference.md` for parsing logic and error-handling patterns.
### Authentication
Shippo uses API-key auth. The MCP plugin reads a single `SHIPPO_API_KEY` env-var from `~/.claude/settings.json` and forwards it as the HTTP header `Mcp-Shippo-Key-Auth-Api-Key-Header: ShippoToken <key>`. Each user supplies their own key.
- **Token format:** `ShippoToken shippo_test_*` (sandbox) or `ShippoToken shippo_live_*` (production). The `ShippoToken ` prefix is required β bare keys are rejected.
- **Where to obtain:** [https://apps.goshippo.com/settings/api](https://apps.goshippo.com/settings/api). Both modes are free to create; only live-mode label purchases incur real charges.
- **One-time setup** β paste the key into the `env` block of `~/.claude/settings.json`, then restart Claude Code (or `/reload-plugins`). Unlike OAuth, there is no per-call authorize flow β the same header is sent on every MCP call until the user rotates the key.
- **Two 401 strings to recognize:**
- `"Token does not exist"` β the key is wrong, revoked, or from the other mode (test key against a live-only resource, etc.). Ask the user to verify the key and prefix at the dashboard.
- `"Authentication credentials were not provided"` β no token reached Shippo. Either `SHIPPO_API_KEY` is unset, the env-var didn't interpolate (check `~/.claude/settings.json` syntax), or an upstream proxy stripped the header.
### Test vs live mode discipline
At the start of any purchase or label workflow, check the API key prefix:
- `shippo_test_*` β Test mode. Labels are free. No real charges. Tracking uses mock numbers (see `shippo/references/test-mode.md`).
- `shippo_live_*` β Live mode. Real charges. Inform the user which mode they are in.
Test and live mode have completely separate data and object IDs. An object ID from one mode will not resolve in the other.
### Key documentation
- [API Concepts](https://docs.goshippo.com/docs/api_concepts/apiversioning) β request shapes, versioning, auth
- [Address Validation Guide](https://docs.goshippo.com/docs/addresses/address_validation) β validation depth varies by country
- [Customs Reference](https://docs.goshippo.com/docs/exporting/internationalshipments) β incoterms, contents types, HS codes
- [Carrier Accounts](https://docs.goshippo.com/docs/shipping/carrieraccounts) β managed vs custom accounts
- [Webhooks](https://docs.goshippo.com/docs/tracking/webhooks) β event types, signature verification
(Once Mintlify migration completes, `.md` URL suffixes will provide raw markdown access for AI agents.)
---
## Address Validation
### Address Field Format
The Shippo API uses **v1 field names** for address components in most endpoints (including `create_shipment`). Always use:
| Field | Description | Example |
|---|---|---|
| `name` | Full name | `Jane Smith` |
| `street1` | Street address line 1 | `731 Market St` |
| `street2` | Street address line 2 (optional) | `Suite 200` |
| `city` | City | `San Francisco` |
| `state` | State or province | `CA` |
| `zip` | Postal code | `94103` |
| `country` | ISO 3166-1 alpha-2 country code | `US` |
| `email` | Email (required for international senders) | `jane@example.com` |
| `phone` | Phone (required for international senders) | `+1-555-123-4567` |
Note: The v2 address endpoints (`create_address_v2`, `validate_address_v2`) use different field names (`address_line_1`, `city_locality`, `state_province`, `postal_code`), but when passing addresses inline to `create_shipment`, you must use the v1 names above.
---
### Validate a Structured Address
1. Collect at minimum: `street1`, `city`, `state`, `zip`, `country` (ISO 3166-1 alpha-2).
2. Call `create_address_v2` with the address fields. This creates the address and returns an object ID.
3. Call `validate_address_v2` with the address fields to get validation results. Note: this endpoint takes address fields as query parameters, not an object ID.
4. Check `analysis.validation_result.value` in the response. Values: `"valid"`, `"invalid"`, or `"partially_valid"` (address found with corrections applied). Check `analysis.validation_result.reasons` for details.
5. Report the standardized address back. Highlight any corrected fields (listed in `changed_attributes`). Note `analysis.address_type` (`"residential"`, `"commercial"`, or `"unknown"`) -- residential classification affects carrier surcharges.
6. If invalid: relay the reason descriptions. If the API returns a `recommended_address`, present it to the user.
7. If `partially_valid`: show what was corrected and ask the user to confirm the corrections are acceptable.
---
### Parse a Freeform Address
1. Call `parse_address` with the raw string (e.g., "123 Main St, Springfield IL 62704").
2. Review the structured output for completeness. The parse response uses v2 field names: `address_line_1`, `city_locality`, `state_province`, `postal_code`.
3. Note: the parse response does not include `country`. You must ask the user for the country or infer it, then add it before proceeding.
4. Validate the parsed result by passing the fields to `create_address_v2` then `validate_address_v2` (follow the structured address workflow above from step 2).
---
### International Addresses
- Always require the `country` field. Do not guess.
- Pass non-Latin characters as-is; the API handles encoding.
- Validation depth varies by country. US, CA, GB, AU, and major EU countries have deep validation. Others may only confirm structural completeness. Inform the user of this limitation.
---
### Bulk Address Validation
There is no batch validation endpoint. Call `create_address_v2` per address. Track results (row number, valid/invalid, corrections, errors, residential classification) and report a summary when done. For 50+ addresses, set expectations about processing time and provide progress updates.
---
### Re-validate an Existing Address
Call `validate_address_v2` with the address fields. This endpoint validates by address fields, not by object ID.
---
### Duplicate Addresses
If `create_address_v2` returns a "Duplicate address" error, the address already exists in the account. Retrieve it via `list_addresses` or proceed directly to validation.
---
### Quick Reference
**Validate an address:**
`create_address_v2` (saves address) + `validate_address_v2` (validates with same fields)
**Parse then validate:**
`parse_address` -> add country -> `create_address_v2` + `validate_address_v2`
---
## Rate Shopping
### Get Rates for a Shipment
1. Collect: origin address, destination address, parcel (length, width, height, distance_unit, weight, mass_unit). All dimension and weight values must be **strings** (e.g., `"10"` not `10`).
2. Optionally validate both addresses with `validate_address_v2` (see Address Validation).
3. Call `create_shipment` with `address_from`, `address_to` (as inline address objects using v1 field names -- `street1`, `city`, `state`, `zip`, `country` -- not object IDs), and `parcels`.
4. The response `rates` array contains available options. Present a table: carrier, service level, price, estimated days.
5. Note: the same carrier may return duplicate rates from multiple carrier accounts. Present the best rate per carrier/service combination.
---
### Rate Expiration
Rates expire after 7 days. If a user tries to purchase a rate that was retrieved more than 7 days ago, create a new shipment to get fresh rates.
---
### Filter by Speed
Map user requests: "overnight" = estimated_days 1, "2-day" = estimated_days <= 2, "within N days" = estimated_days <= N. Filter the rates array accordingly. If nothing matches, show the fastest available option.
---
### International Rates
Some carriers may return international rates without a customs declaration, but others will not. If no rates are returned, try attaching a customs declaration to the shipment. Some carriers also require a phone number on the destination address for international rate retrieval. Inform the user that customs will be required at label purchase time regardless. See `references/customs-guide.md` for customs details.
---
### Checkout Rates (Line Items)
Call `generate_live_rates` instead of `create_shipment`. Accepts `address_from`, `address_to`, and `line_items` (each with title, quantity, total_price, currency, weight, weight_unit).
---
### Rates in a Specific Currency
Call `list_shipment_rates_by_currency` with the preferred ISO currency code (USD, EUR, GBP, CAD, etc.).
---
### Recommendation
Identify the cheapest (lowest `amount`), fastest (lowest `estimated_days`), and best-value options from the rates array. These are not API fields -- compute them by sorting the rates array yourself. State the trade-off: "Option A is $X cheaper but takes Y more days than Option B."
---
### Troubleshooting: No Rates
- Verify both addresses passed validation (most common cause).
- Confirm parcel dimensions are reasonable (not zero, not exceeding carrier limits).
- Shippo provides managed carrier accounts by default for major carriers. If no rates are returned, the issue is more likely address validation, unsupported route, or parcel dimensions -- not missing carrier accounts. You can verify with `list_carrier_accounts` if needed.
- Rates expire after 7 days. If stale, create a new shipment to get fresh rates.
---
### Quick Reference
**Get rates:**
(optional) `validate_address_v2` (x2) -> `create_shipment` (with inline addresses) -> read `rates` array
---
## Label Purchase
### Test vs Live Mode
At the start of any label purchase workflow, check the API key prefix:
- **Test keys** (`shippo_test_*`): Labels are free. No charges are incurred. Use for testing workflows.
- **Live keys** (`shippo_live_*`): Labels incur real charges. Inform the user which mode they are in before proceeding. **On a live key, explicitly state "this will charge your live Shippo account" and require the user to acknowledge before purchasing.** Do not proceed on a live key without that acknowledgement.
---
### Purchase Confirmation Gate
Before every call to `purchase_shipping_label`, summarize the following and ask the user for explicit confirmation:
- Carrier and service level
- Estimated cost
- Estimated delivery time
- Origin and destination
**Do not proceed without explicit user confirmation.**
---
### Domestic Label
1. Optionally validate both addresses with `validate_address_v2` (see Address Validation).
2. Call `create_shipment` with `address_from`, `address_to` (as inline address objects using v1 field names -- `street1`, `city`, `state`, `zip`, `country`), `parcels`, and `async: false`.
3. Present rates to the user. Let them choose.
4. **Confirm purchase** (see Purchase Confirmation Gate above).
5. Call `purchase_shipping_label` with: `rate` (selected rate object_id), `label_file_type` (default `PDF_4x6`), `async: false`.
6. Check response `status`:
- `SUCCESS`: return `tracking_number`, `label_url` (display the COMPLETE URL -- S3 signed URLs break if truncated), and `tracking_url_provider`.
- `QUEUED`/`WAITING`: poll `get_transaction` until resolved.
- `ERROR`: report messages from the `messages` array.
---
### International Label
All domestic steps apply, plus customs handling before shipment creation. See `references/customs-guide.md` for the full customs workflow.
1. Optionally validate addresses with `validate_address_v2`. Sender must include `email` and `phone`. Ask if missing.
2. Create customs items: call `create_customs_item` per item (description, quantity, net_weight, mass_unit, value_amount, value_currency, origin_country, tariff_number). Alternatively, you can skip this step and pass inline item objects directly in the declaration (step 3).
3. Create the customs declaration: call `create_customs_declaration` with contents_type, non_delivery_option, certify: true, certify_signer, and the items (either object_ids from step 2, or inline item objects). See `references/customs-guide.md` for field details.
4. Call `create_shipment` with all standard fields plus `customs_declaration` (the declaration object_id).
5. Present rates, **confirm purchase** (see Purchase Confirmation Gate), then purchase label and return results as in the domestic flow.
#### Contents Type Decision Tree
Use this to determine the correct `contents_type` value:
| Scenario | Value |
|---|---|
| Selling to the recipient (commercial sale) | `MERCHANDISE` |
| Sending a free gift | `GIFT` |
| Sending a product sample | `SAMPLE` |
| Paper documents only | `DOCUMENTS` |
| Customer returning a purchased item | `RETURN_MERCHANDISE` |
| Charitable donation | `HUMANITARIAN_DONATION` |
| None of the above | `OTHER` (requires `contents_explanation`) |
#### Incoterms Decision Logic
The `incoterm` field on the customs declaration controls who pays duties and taxes:
- **B2C / e-commerce (default):** Use `DDU` (Delivered Duty Unpaid) -- recipient pays duties at delivery.
- **Seller prepays duties:** Use `DDP` (Delivered Duty Paid) -- seller covers all duties and taxes.
- **FedEx/DHL only:** `FCA` (Free Carrier) is available for advanced trade scenarios.
If the user does not specify, default to `DDU` for standard e-commerce shipments.
---
### Return Labels
To generate a return label, swap `address_from` and `address_to` so the original recipient becomes the sender and the original sender becomes the recipient. All other steps (shipment creation, rate selection, label purchase) remain the same.
---
### Label Format Options
Default to `PDF_4x6` unless the user specifies otherwise. Supported formats: `PDF_4x6`, `PDF_4x8`, `PDF_A4`, `PDF_A5`, `PDF_A6`, `PDF`, `PDF_2.3x7.5`, `PNG`, `PNG_2.3x7.5`, `ZPLII`.
---
### Label Customization Options
When purchasing a label via `purchase_shipping_label`, the following options may be set on the shipment or rate:
- **Signature confirmation**: set `signature_confirmation` on the shipment's `extra` field. Values: `STANDARD`, `ADULT`, `CERTIFIED`, `INDIRECT`, `CARRIER_CONFIRMATION`.
- **Insurance**: set `insurance` on the shipment's `extra` field with `amount`, `currency`, and `provider`.
- **Saturday delivery**: set `saturday_delivery` to `true` in the shipment's `extra` field. Only supported by certain carriers and service levels.
- **Reference fields**: pass `metadata` on the transaction for order numbers or internal references.
---
### Label from Existing Rate
If the user already has a rate object_id: optionally call `get_rate` to confirm details, then **confirm purchase** (see Purchase Confirmation Gate), then call `purchase_shipping_label` directly.
---
### Voiding a Label
Call `request_refund` with the transaction object_id.
**Refund limitations:** Void/refund eligibility depends on carrier and timing. Not all labels can be refunded after purchase. If `request_refund` fails, advise the user to contact Shippo support.
---
### Quick Reference
**Domestic label:**
(optional) `validate_address_v2` (x2) -> `create_shipment` (with inline addresses) -> user picks rate -> confirm -> `purchase_shipping_label`
**International label:**
(optional) `validate_address_v2` (x2) -> `create_customs_item` (per item) -> `create_customs_declaration` -> `create_shipment` (with inline addresses + customs_declaration) -> user picks rate -> confirm -> `purchase_shipping_label`
**Return label:**
Same as domestic/international, but swap `address_from` and `address_to`.
**Order-to-label:**
`create_order` -> `create_shipment` (using order address/item data) -> user picks rate -> confirm -> `purchase_shipping_label` -> `orders-get-packing-slip`
---
### Orders and Packing Slips
Use orders to represent e-commerce fulfillment requests. An order captures the shipping address, line items, and totals -- then feeds into the standard label purchase workflow.
#### Tools
- **`create_order`**: Create an order with line items, shipping address, and order details.
- **`get_order`**: Retrieve an order by its object_id.
- **`list_orders`**: List all orders.
- **`orders-get-packing-slip`**: Generate a packing slip PDF for an order. **Known gap:** this tool is not yet in the MCP catalog. The underlying REST endpoint exists at `GET /orders/{ORDER_ID}/packingslip/` (returns a 24-hour S3 PDF link). If the MCP rejects the tool name, fall back to a direct REST call or advise the user to use the Shippo dashboard until the MCP gap is closed.
#### Workflow
1. Call `create_order` with the shipping address, line items (title, quantity, sku, total_price, etc.), and order-level fields.
2. Use the order's address and item data to call `create_shipment`, then follow the standard label purchase flow (rate selection, confirmation, `purchase_shipping_label`).
3. After purchasing the label, generate a packing slip via `orders-get-packing-slip` (see Tools above for the known MCP gap and REST fallback).
---
## Tracking
### Track by Number
1. Determine carrier and tracking number. Carrier must be a lowercase Shippo token (e.g., `usps`, `ups`, `fedex`, `dhl_express`). See `references/carrier-guide.md` for tracking number format hints per carrier. If uncertain, ask the user.
2. Call `get_tracking_status` with `carrier` and `tracking_number`.
- In test mode, use `shippo` as the carrier β see `references/test-mode.md` for details.
3. Key response fields: `tracking_status` (status, status_details, status_date, location), `tracking_history`, `eta`.
4. Each tracking event includes a `substatus` object with `code`, `text`, and `action_required` (boolean). Include substatus details when presenting tracking history -- these provide more specific information about what happened at each step.
5. Present: current status, location, ETA, substatus details, and chronological event history (most recent first).
---
### Test Mode
See `references/test-mode.md` for mock tracking numbers and test mode behavior. Key points: use `shippo` as the carrier token and one of the `SHIPPO_*` mock tracking numbers (e.g., `SHIPPO_TRANSIT`, `SHIPPO_DELIVERED`).
---
### Status Values
See `references/carrier-guide.md` for carrier-specific status nuances. Standard values:
| Status | Meaning |
|---|---|
| PRE_TRANSIT | Label created, carrier has not received the package |
| TRANSIT | Package is in transit |
| DELIVERED | Delivered |
| RETURNED | Being returned or returned to sender |
| FAILURE | Delivery failed |
| UNKNOWN | No tracking information from carrier |
The `eta` field is provided by most major carriers (USPS, UPS, FedEx, DHL Express) but availability is carrier-dependent β it may be `null` for regional carriers or for shipments before the carrier has finalized routing. Treat absence as informational, not as an error condition.
---
### Find Trackable Packages
Call `list_transactions`. Filter for `object_status: SUCCESS`. Each successful transaction has `tracking_number` and carrier info. Then call `get_tracking_status` for selected items.
---
### Register a Tracking Webhook
1. Get the user's HTTPS webhook URL.
2. Call `create_webhook` with `url` and `event: track_updated`.
3. Optionally call `register_tracking` with carrier and tracking number to register a specific shipment for push updates.
---
### Quick Reference
**Track a package:**
`get_tracking_status` with carrier + tracking number
**Find past shipment tracking:**
`list_transactions` -> filter SUCCESS -> `get_tracking_status`
---
## Batch Shipping
### Test vs Live Mode
At the start of any batch purchase workflow, check the API key prefix:
- **Test keys** (`shippo_test_*`): Labels are free. No charges are incurred.
- **Live keys** (`shippo_live_*`): Labels incur real charges. Inform the user which mode they are in before proceeding.
---
### Purchase Confirmation Gate
Before every call to `purchase_batch_labels`, summarize the following and ask the user for explicit confirmation:
- Total number of shipments to be purchased
- Carrier and service level (or selection rule if varied)
- Estimated total cost
- Number of domestic vs international shipments
**Do not proceed without explicit user confirmation.**
---
### CSV Batch Processing
See `references/csv-format.md` for the column specification.
1. Read and parse the CSV. Validate required columns are present. Report row count.
2. Validate each row for non-empty required fields. Report invalid rows with reasons.
3. Detect international rows (sender_country != recipient_country). Create customs declarations for those rows. See `references/customs-guide.md`. Use correct customs enum values: `RETURN_MERCHANDISE` (not `RETURN`) for returned goods, `HUMANITARIAN_DONATION` (not `HUMANITARIAN`) for charitable donations.
4. Build the `batch_shipments` array with inline address and parcel objects per row.
5. Call `create_label_batch` with the array.
6. Poll `get_batch` until status changes from `VALIDATING` to `VALID`. See Polling Intervals below.
7. Review per-shipment validation results. Report failures before proceeding.
8. **Confirm purchase** (see Purchase Confirmation Gate above).
9. Call `purchase_batch_labels` to buy labels for all valid shipments.
10. Poll `get_batch` until status changes from `PURCHASING` to `PURCHASED`. See Polling Intervals below.
11. Report: total attempted, succeeded, failed. For successes: tracking_number and label_url (complete URL). For failures: error messages.
#### Batch Size Guidance
For batches over 500 shipments, consider splitting into multiple batches. Large batches take longer to validate and purchase, and a single failure can be harder to diagnose.
---
### Polling Intervals
- For batches under 100 shipments: poll every 3-5 seconds.
- For batches with 100+ shipments: poll every 5-10 seconds.
- Report progress to the user every 30 seconds.
- Stop after 60 retries and suggest the user check back later using `get_batch` with the batch object_id.
---
### Batch with Rate Shopping
1. Call `create_shipment` per shipment to get rate quotes (see Rate Shopping).
2. Present rates. User picks a service level rule (e.g., "cheapest for each" or a specific carrier/service).
3. Build `batch_shipments` with `servicelevel_token` per item.
4. Create, validate, **confirm purchase**, purchase, report as above.
---
### Managing an Existing Batch
- Add shipments: `add_shipments_to_batch` (before purchase only). Note: adding an invalid shipment will change the entire batch status to `INVALID`. Check per-shipment statuses after adding.
- Remove shipments: `remove_shipments_from_batch` (before purchase only).
---
### End-of-Day Manifest
1. Collect: `carrier_account` (object_id), `shipment_date` (YYYY-MM-DD, default today), `address_from` (pickup address).
2. Optionally collect specific transaction object_ids to scope the manifest. You must pass specific transaction object_ids -- there is no auto-include for a date range.
3. Call `create_end_of_day_manifest`.
4. Poll `get_manifest` until status is `SUCCESS` or `ERROR`.
5. Return the manifest PDF URL(s) and shipment count.
---
### Quick Reference
**CSV batch:**
Parse CSV -> `create_customs_declaration` (international rows) -> `create_label_batch` -> poll `get_batch` -> confirm -> `purchase_batch_labels` -> poll `get_batch`
**Manifest:**
`create_end_of_day_manifest` (with transaction object_ids) -> poll `get_manifest`
---
## Shipping Analysis
### Geographic Cost Analysis
1. Confirm origin address, destination list (or use representative cities), and parcel details.
2. Call `list_carrier_accounts` to see configured carriers.
3. Call `create_shipment` per destination to collect rates. Creating shipments is free; only `purchase_shipping_label` costs money.
4. Write results to `analysis/` directory (markdown report + CSV). Columns: Route, Destination, Carrier, Service, Cost, Currency, EstimatedDays, Zone.
---
### Package Optimization
1. Confirm the route.
2. Define dimension profiles to test (or use user-provided ones).
3. Check `list_carrier_parcel_templates` and `list_user_parcel_templates` for flat-rate and saved templates. See `references/rate-shopping-guide.md` for dimensional weight and flat-rate guidance.
4. Call `create_shipment` per profile on the same route.
5. Compare: cheapest rate, carrier options, fastest option per profile. Note where flat-rate templates beat custom dimensions and where dimensional weight causes price jumps. See `references/carrier-guide.md` for carrier-specific weight limits and surcharges.
---
### Carrier Comparison
1. Call `create_shipment` for the route.
2. Group the `rates` array by `provider`.
3. Per carrier: cheapest service, fastest service, number of service levels, price range.
---
### Historical Cost Optimization
1. Call `list_shipments` and `list_transactions` to get past activity.
2. Cross-reference: what the user paid vs. what alternatives were available.
3. Identify patterns: carrier concentration, service-level mismatch, consistent overpayment.
4. For a sample of shipments with tracking numbers, call `get_tracking_status` to check actual vs. estimated delivery times.
5. If fewer than 5 successful transactions exist (not just shipments -- shipments are rate quotes, transactions represent actual spend), redirect to forward-looking analysis.
---
### Output Conventions
Write reports to the `analysis/` directory. Create it if it does not exist. Include both markdown and CSV. CSV must have a header row. Markdown must include a timestamp and input parameters.
---
### Quick Reference
**Cost analysis:**
`list_carrier_accounts` -> `create_shipment` (per destination) -> read `rates` arrays -> write report
**Carrier comparison:**
`create_shipment` -> group `rates` by `provider` -> summarize
**Historical review:**
`list_shipments` + `list_transactions` -> cross-reference -> `get_tracking_status` (sample) -> write report
---
## Upgrades
The current Shippo API version is **2018-02-08**. Shippo uses a single long-lived API version sent via the `Shippo-API-Version` header.
### API version handling
The API version is set per-request via the `Shippo-API-Version` header:
```
Shippo-API-Version: 2018-02-08
```
Most version changes are backward-compatible (new optional fields, new resources, additional webhook events). Breaking changes are rare and announced via release notes.
If you don't pin the version, the API uses the version associated with your account at the time of request. Pin explicitly for reproducibility.
### SDK upgrades
#### Python (`shippo`)
```bash
pip install --upgrade shippo
```
[Package on PyPI](https://pypi.org/project/shippo/) | [Python SDK reference](https://docs.goshippo.com/sdks/python)
#### JavaScript / TypeScript
```bash
npm install --save shippo@latest
```
For the MCP server (separate package):
```bash
npm install -g @shippo/shippo-mcp@latest
```
[`shippo` on npm](https://www.npmjs.com/package/shippo) | [`@shippo/shippo-mcp` on npm](https://www.npmjs.com/package/@shippo/shippo-mcp)
#### Other SDKs
Ruby, Go, PHP, Java, .NET β see [Shippo SDK directory](https://docs.goshippo.com/sdks).
### MCP server upgrade
The Shippo MCP server is distributed two ways. They have different upgrade paths:
1. **Hosted Gram MCP** β the hosted MCP server (URL configured in your `.mcp.json`). Auto-updated server-side. No client action needed.
2. **Local npm package** `@shippo/shippo-mcp` β Update via `npm update -g @shippo/shippo-mcp` (or `npm install -g @shippo/shippo-mcp@latest`).
After updating the local npm package, restart your MCP client (Claude Desktop, Cursor, Claude Code) so the new server binary is picked up.
### Breaking changes log
Shippo API changes are tracked in [the API changelog](https://docs.goshippo.com/changelog).
When a breaking change ships that affects the workflows in this skill set, this section will be updated with migration guidance.
(As of 2026-05, no recent breaking changes affect the workflows covered by this skill set.)
### Webhook event versioning
Webhook events can include new fields without bumping API version. To handle a new field gracefully:
- Default to ignoring unknown fields in your webhook handler β never fail-closed on a field you don't recognize.
- Subscribe only to the specific event types you need (`track_updated`, `transaction_created`, `transaction_updated`, etc.).
- Verify webhook signatures using the `Shippo-Signature` header per [webhook docs](https://docs.goshippo.com/docs/tracking/webhooks).
### Troubleshooting upgrades
#### `ReferenceError: Response is not defined` after updating `@shippo/shippo-mcp`
Cause: Node version <18, or stale npx cache.
Fix:
1. Update Node to v20 or later (`nvm use 20`).
2. Clear npx cache: `npm cache clean --force && rm -rf ~/.npm/_npx/`.
3. Restart your MCP client.
#### Tools missing after MCP update
The MCP tool catalog is cached client-side. Restart Claude Desktop / Cursor / Claude Code to refresh.
#### Response shape changed unexpectedly
Speakeasy wraps API responses in an envelope. If you're seeing unexpected response shapes after an upgrade, see `shippo/references/response-envelope.md` for envelope structure and parsing logic.
#### `401` or `403` errors after upgrade
Verify your `SHIPPO_API_KEY` is still valid and has the correct prefix (`ShippoToken shippo_test_*` or `ShippoToken shippo_live_*`). API keys can be regenerated in the [Shippo Dashboard](https://apps.goshippo.com/settings/api).
#### "Not found" errors for objects you created before the upgrade
Most likely a test/live mode mismatch. Test and live mode have separate object ID spaces β an object created in test mode is not visible in live mode. See `shippo/references/test-mode.md`.
### Auditing an existing integration
Before upgrading a production integration:
1. Pin the current API version explicitly via the `Shippo-API-Version` header so future server-side defaults don't shift behavior.
2. Verify webhook handlers ignore unknown fields.
3. Review the [API changelog](https://docs.goshippo.com/changelog) for any breaking changes between your current version and the latest.
4. Test in test mode (`shippo_test_*` API key) before deploying to live.
---
## Error Handling
- **Never guess** parcel dimensions, weight, customs values, HS codes, or signer names. Ask the user.
- **Do not auto-retry** transport, auth, or rate-limit errors. Report to user and stop.
- Parcel dimensions and weight must be **strings** (e.g., `"10"` not `10`).
- Label URLs are S3 signed URLs. **Always display the complete URL** -- truncating breaks the signature.
- Rates expire after 7 days. Create a new shipment for fresh rates.
- No rates? Validate addresses first, then check dimensions, then `carrier-accounts-list`.
- "Not found" errors: verify API key mode matches the data -- test and live have separate object IDs.
---
## Security & Data Transparency
- **Default (Gram-hosted) path:** requests are routed via [Gram](https://app.getgram.ai) β a Speakeasy-operated hosted MCP gateway. The Gram MCP server at `https://app.getgram.ai/mcp/shippo-key-auth` accepts your API key via the `Mcp-Shippo-Key-Auth-Api-Key-Header: ShippoToken <key>` header and forwards each Shippo API call to `api.goshippo.com` with that same auth header. No local Node process is needed for clients that support `type: http` with custom headers.
- **Self-host alternative:** if you'd prefer no third-party gateway in the path, the local `@shippo/shippo-mcp` npm package (alternative config in Setup) talks directly to `api.goshippo.com` via stdio. The `SHIPPO_API_KEY` is passed to the MCP via the `--api-key-header` CLI flag and forwarded to Shippo as an `Authorization: ShippoToken <key>` header.
- No data is stored by the skill itself; all persistence is handled by Shippo's API.
- Label and tracking data are subject to Shippo's data retention policies.
don't have the plugin yet? install it then click "run inline in claude" again.