Install
openclaw skills install goshippo(Beta) Ship packages with Shippo. Multi-carrier rate shopping, label generation, package tracking, address validation, customs declarations, and batch processing from CSV files.
openclaw skills install goshippo⚠️ Breaking change in v1.1.0 — set
SHIPPO_API_KEYto the bare token (noShippoTokenprefix)Previous docs said the env var should be
ShippoToken shippo_test_xxx. That format double-prefixed the auth header (the args/headers block addsShippoTokenautomatically) and would produce auth failures.Update your env var to just the token value (no
ShippoTokenprefix; the MCP config in the examples below prepends it for you). The value should look like<your-shippo-key>— a token starting withshippo_test_(sandbox) orshippo_live_(production). Grab one from the Shippo dashboard.
MCP server (default): Shippo-hosted via Gram (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:
{
"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 stdio-to-HTTP bridge to the same Gram URL (requires Node.js 18+):
{
"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}"
]
}
}
}
If you'd rather not route through Gram, the @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+.
{
"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.
Latest Shippo API version: 2018-02-08. Send via the Shippo-API-Version header.
| 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.
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.purchase_shipping_label. Show carrier/service/cost/eta and require explicit user confirmation."10", never 10.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.
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.
ShippoToken shippo_test_* (sandbox) or ShippoToken shippo_live_* (production). The ShippoToken prefix is required — bare keys are rejected.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."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.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.
(Once Mintlify migration completes, .md URL suffixes will provide raw markdown access for AI agents.)
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.
street1, city, state, zip, country (ISO 3166-1 alpha-2).create_address_v2 with the address fields. This creates the address and returns an object ID.validate_address_v2 with the address fields to get validation results. Note: this endpoint takes address fields as query parameters, not an object ID.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.changed_attributes). Note analysis.address_type ("residential", "commercial", or "unknown") -- residential classification affects carrier surcharges.recommended_address, present it to the user.partially_valid: show what was corrected and ask the user to confirm the corrections are acceptable.parse_address with the raw string (e.g., "123 Main St, Springfield IL 62704").address_line_1, city_locality, state_province, postal_code.country. You must ask the user for the country or infer it, then add it before proceeding.create_address_v2 then validate_address_v2 (follow the structured address workflow above from step 2).country field. Do not guess.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.
Call validate_address_v2 with the address fields. This endpoint validates by address fields, not by object ID.
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.
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
"10" not 10).validate_address_v2 (see Address Validation).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.rates array contains available options. Present a table: carrier, service level, price, estimated days.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.
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.
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.
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).
Call list_shipment_rates_by_currency with the preferred ISO currency code (USD, EUR, GBP, CAD, etc.).
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."
list_carrier_accounts if needed.Get rates:
(optional) validate_address_v2 (x2) -> create_shipment (with inline addresses) -> read rates array
At the start of any label purchase workflow, check the API key prefix:
shippo_test_*): Labels are free. No charges are incurred. Use for testing workflows.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.Before every call to purchase_shipping_label, summarize the following and ask the user for explicit confirmation:
Do not proceed without explicit user confirmation.
validate_address_v2 (see Address Validation).create_shipment with address_from, address_to (as inline address objects using v1 field names -- street1, city, state, zip, country), parcels, and async: false.purchase_shipping_label with: rate (selected rate object_id), label_file_type (default PDF_4x6), async: false.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.All domestic steps apply, plus customs handling before shipment creation. See references/customs-guide.md for the full customs workflow.
validate_address_v2. Sender must include email and phone. Ask if missing.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).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.create_shipment with all standard fields plus customs_declaration (the declaration object_id).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) |
The incoterm field on the customs declaration controls who pays duties and taxes:
DDU (Delivered Duty Unpaid) -- recipient pays duties at delivery.DDP (Delivered Duty Paid) -- seller covers all duties and taxes.FCA (Free Carrier) is available for advanced trade scenarios.If the user does not specify, default to DDU for standard e-commerce shipments.
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.
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.
When purchasing a label via purchase_shipping_label, the following options may be set on the shipment or rate:
signature_confirmation on the shipment's extra field. Values: STANDARD, ADULT, CERTIFIED, INDIRECT, CARRIER_CONFIRMATION.insurance on the shipment's extra field with amount, currency, and provider.saturday_delivery to true in the shipment's extra field. Only supported by certain carriers and service levels.metadata on the transaction for order numbers or internal references.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.
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.
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
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.
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.create_order with the shipping address, line items (title, quantity, sku, total_price, etc.), and order-level fields.create_shipment, then follow the standard label purchase flow (rate selection, confirmation, purchase_shipping_label).orders-get-packing-slip (see Tools above for the known MCP gap and REST fallback).usps, ups, fedex, dhl_express). See references/carrier-guide.md for tracking number format hints per carrier. If uncertain, ask the user.get_tracking_status with carrier and tracking_number.
shippo as the carrier — see references/test-mode.md for details.tracking_status (status, status_details, status_date, location), tracking_history, eta.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.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).
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.
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.
create_webhook with url and event: track_updated.register_tracking with carrier and tracking number to register a specific shipment for push updates.Track a package:
get_tracking_status with carrier + tracking number
Find past shipment tracking:
list_transactions -> filter SUCCESS -> get_tracking_status
At the start of any batch purchase workflow, check the API key prefix:
shippo_test_*): Labels are free. No charges are incurred.shippo_live_*): Labels incur real charges. Inform the user which mode they are in before proceeding.Before every call to purchase_batch_labels, summarize the following and ask the user for explicit confirmation:
Do not proceed without explicit user confirmation.
See references/csv-format.md for the column specification.
references/customs-guide.md. Use correct customs enum values: RETURN_MERCHANDISE (not RETURN) for returned goods, HUMANITARIAN_DONATION (not HUMANITARIAN) for charitable donations.batch_shipments array with inline address and parcel objects per row.create_label_batch with the array.get_batch until status changes from VALIDATING to VALID. See Polling Intervals below.purchase_batch_labels to buy labels for all valid shipments.get_batch until status changes from PURCHASING to PURCHASED. See Polling Intervals below.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.
get_batch with the batch object_id.create_shipment per shipment to get rate quotes (see Rate Shopping).batch_shipments with servicelevel_token per item.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_from_batch (before purchase only).carrier_account (object_id), shipment_date (YYYY-MM-DD, default today), address_from (pickup address).create_end_of_day_manifest.get_manifest until status is SUCCESS or ERROR.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
list_carrier_accounts to see configured carriers.create_shipment per destination to collect rates. Creating shipments is free; only purchase_shipping_label costs money.analysis/ directory (markdown report + CSV). Columns: Route, Destination, Carrier, Service, Cost, Currency, EstimatedDays, Zone.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.create_shipment per profile on the same route.references/carrier-guide.md for carrier-specific weight limits and surcharges.create_shipment for the route.rates array by provider.list_shipments and list_transactions to get past activity.get_tracking_status to check actual vs. estimated delivery times.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.
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
The current Shippo API version is 2018-02-08. Shippo uses a single long-lived API version sent via the Shippo-API-Version header.
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.
shippo)pip install --upgrade shippo
Package on PyPI | Python SDK reference
npm install --save shippo@latest
For the MCP server (separate package):
npm install -g @shippo/shippo-mcp@latest
shippo on npm | @shippo/shippo-mcp on npm
Ruby, Go, PHP, Java, .NET — see Shippo SDK directory.
The Shippo MCP server is distributed two ways. They have different upgrade paths:
.mcp.json). Auto-updated server-side. No client action needed.@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.
Shippo API changes are tracked in the API 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 events can include new fields without bumping API version. To handle a new field gracefully:
track_updated, transaction_created, transaction_updated, etc.).Shippo-Signature header per webhook docs.ReferenceError: Response is not defined after updating @shippo/shippo-mcpCause: Node version <18, or stale npx cache.
Fix:
nvm use 20).npm cache clean --force && rm -rf ~/.npm/_npx/.The MCP tool catalog is cached client-side. Restart Claude Desktop / Cursor / Claude Code to refresh.
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 upgradeVerify 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.
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.
Before upgrading a production integration:
Shippo-API-Version header so future server-side defaults don't shift behavior.shippo_test_* API key) before deploying to live."10" not 10).carrier-accounts-list.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.@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.