Install
openclaw skills install goshippo(Beta) A shipping and logistics skill for Shippo. Get multi-carrier rates (USPS, UPS, FedEx, DHL, 30+), buy domestic and international labels with customs, validate addresses, track packages with webhooks, and run bulk CSV batches, plus cost analysis, integration routing, and SDK-upgrade help. Runs through Shippo's hosted MCP with per-user OAuth (sign in once, nothing to copy or store). Uses Shippo's discounted carrier rates.
openclaw skills install goshippoMCP server: Shippo's hosted MCP at https://mcp.shippo.com, with per-user Shippo OAuth. You authorize once through Shippo on first use, with nothing to copy or configure, and the client refreshes the token automatically.
Point your MCP client at the hosted server:
{
"mcpServers": {
"shippo": {
"type": "http",
"url": "https://mcp.shippo.com"
}
}
}
On first use, your client runs the Shippo OAuth sign-in (in OpenClaw, openclaw mcp login shippo; in Claude Code, /mcp). No local Node process and nothing to store.
Prerequisites: A Shippo account 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.
Purchases are live: label purchases charge the authorized Shippo account for real. Confirm carrier, service, and cost with the user before any purchase.
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.
The hosted Shippo MCP at https://mcp.shippo.com exposes exactly 4 tools (a meta-API), not the underlying operations directly:
shippo_list_tools: discover which operation you need.shippo_describe_tool: get that operation's input schema.shippo_read_execute_tool: run a read (lists, gets, lookups).shippo_write_execute_tool: run a write or mutation (creates, purchases, voids).Every operation name in this skill (ValidateAddress, CreateShipment, CreateTransaction, GetTrack, etc.) is invoked through these wrappers, never called as a tool on its own. Standard discovery pattern: shippo_list_tools to find the operation, then shippo_describe_tool for its schema, then shippo_read_execute_tool or shippo_write_execute_tool to run it. The read/write split lets approval policies gate mutations separately. In the Claude apps these 4 tools may be deferred (loaded on demand), so an initial "tool has not been loaded yet" is normal: discover via the wrappers rather than guessing operation names.
| 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 |
| 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.
CreateTransaction. 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.
The hosted MCP at https://mcp.shippo.com uses per-user Shippo OAuth. You authorize once through Shippo (in Claude Code, run /mcp and sign in), and the session refreshes automatically. There is nothing to copy or configure. Once you are connected, the workflow guidance below is unchanged.
"Token does not exist": the credential is invalid, revoked, or for a different account. Re-authorize the Shippo OAuth session."Authentication credentials were not provided": no credential reached Shippo. The OAuth session is not authorized yet, or it has expired. Re-authorize the Shippo OAuth session.Label and batch purchases charge the authorized Shippo account for real money. Before any CreateTransaction or PurchaseBatch, show the carrier, service level, cost, and ETA, and get explicit user confirmation. Do not proceed without it.
(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 CreateShipment). 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: CreateAddress and ValidateAddress take the v2 field names (address_line_1, city_locality, state_province, postal_code), but when passing addresses inline to CreateShipment, you must use the v1 names above.
street1, city, state, zip, country (ISO 3166-1 alpha-2).CreateAddress with the address fields. This creates the address and returns an object ID.ValidateAddress 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.ParseAddress 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.CreateAddress then ValidateAddress (follow the structured address workflow above from step 2).country field. Do not guess.There is no batch validation endpoint. Call CreateAddress 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 ValidateAddress with the address fields. This endpoint validates by address fields, not by object ID.
If CreateAddress returns a "Duplicate address" error, the address already exists in the account. Retrieve it via ListAddresses or proceed directly to validation.
Validate an address:
CreateAddress (saves address) + ValidateAddress (validates with same fields)
Parse then validate:
ParseAddress -> add country -> CreateAddress + ValidateAddress
"10" not 10).ValidateAddress (see Address Validation).CreateShipment 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.object_id. To buy a label, pass the chosen rate's object_id to the purchase flow (see Label Purchase); you do not re-send the address or parcel.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 CreateLiveRate instead of CreateShipment. Accepts address_from, address_to, and line_items (each with title, quantity, total_price, currency, weight, weight_unit).
Call ListShipmentRatesByCurrencyCode 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."
ListCarrierAccounts if needed.Get rates:
(optional) ValidateAddress (x2) -> CreateShipment (with inline addresses) -> read rates array
Label purchases charge the authorized Shippo account for real. Before purchasing, explicitly state "this will charge your Shippo account" with the carrier, service, and cost, and require the user to acknowledge. Do not purchase without that confirmation.
Before every call to CreateTransaction, summarize the following and ask the user for explicit confirmation:
Do not proceed without explicit user confirmation.
ValidateAddress (see Address Validation).CreateShipment with address_from, address_to (as inline address objects using v1 field names -- street1, city, state, zip, country), parcels, and async: false.CreateTransaction 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 GetTransaction 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.
ValidateAddress. Sender must include email and phone. Ask if missing.CreateCustomsItem 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).CreateCustomsDeclaration 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.CreateShipment 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 CreateTransaction, 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 GetRate to confirm details, then confirm purchase (see Purchase Confirmation Gate), then call CreateTransaction directly.
Call CreateRefund with the transaction object_id.
Refund limitations: Void/refund eligibility depends on carrier and timing. Not all labels can be refunded after purchase. If CreateRefund fails, advise the user to contact Shippo support.
Domestic label:
(optional) ValidateAddress (x2) -> CreateShipment (with inline addresses) -> user picks rate -> confirm -> CreateTransaction
International label:
(optional) ValidateAddress (x2) -> CreateCustomsItem (per item) -> CreateCustomsDeclaration -> CreateShipment (with inline addresses + customs_declaration) -> user picks rate -> confirm -> CreateTransaction
Return label:
Same as domestic/international, but swap address_from and address_to.
Order-to-label:
CreateOrder -> CreateShipment (using order address/item data) -> user picks rate -> confirm -> CreateTransaction -> packing slip (REST fallback, see below)
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.
CreateOrder: Create an order with line items, shipping address, and order details.GetOrder: Retrieve an order by its object_id.ListOrders: List all orders.GET /orders/{ORDER_ID}/packingslip/ (returns a 24-hour S3 PDF link). Fall back to a direct REST call, or advise the user to use the Shippo dashboard until the MCP gap is closed.CreateOrder with the shipping address, line items (title, quantity, sku, total_price, etc.), and order-level fields.CreateShipment, then follow the standard label purchase flow (rate selection, confirmation, CreateTransaction).usps, ups, fedex, dhl_express). See references/carrier-guide.md for tracking number format hints per carrier. If uncertain, ask the user.GetTrack with carrier and tracking_number.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/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 ListTransactions. Filter for object_status: SUCCESS. Each successful transaction has tracking_number and carrier info. Then call GetTrack for selected items.
createWebhook with url and event: track_updated.CreateTrack with carrier and tracking number to register a specific shipment for push updates.Track a package:
GetTrack with carrier + tracking number
Find past shipment tracking:
ListTransactions -> filter SUCCESS -> GetTrack
Batch purchases charge the authorized Shippo account for real. Before PurchaseBatch, show the shipment count, carrier/service, and estimated total cost, and require explicit user confirmation.
Before every call to PurchaseBatch, 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.CreateBatch with the array.GetBatch until status changes from VALIDATING to VALID. See Polling Intervals below.PurchaseBatch to buy labels for all valid shipments.GetBatch 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.
GetBatch with the batch object_id.CreateShipment per shipment to get rate quotes (see Rate Shopping).batch_shipments with servicelevel_token per item.AddShipmentsToBatch (before purchase only). Note: adding an invalid shipment will change the entire batch status to INVALID. Check per-shipment statuses after adding.RemoveShipmentsFromBatch (before purchase only).carrier_account (object_id), shipment_date (YYYY-MM-DD, default today), address_from (pickup address).CreateManifest.GetManifest until status is SUCCESS or ERROR.CSV batch:
Parse CSV -> CreateCustomsDeclaration (international rows) -> CreateBatch -> poll GetBatch -> confirm -> PurchaseBatch -> poll GetBatch
Manifest:
CreateManifest (with transaction object_ids) -> poll GetManifest
ListCarrierAccounts to see configured carriers.CreateShipment per destination to collect rates. Creating shipments is free; only CreateTransaction costs money.analysis/ directory (markdown report + CSV). Columns: Route, Destination, Carrier, Service, Cost, Currency, EstimatedDays, Zone.ListCarrierParcelTemplates and ListUserParcelTemplates for flat-rate and saved templates. See references/rate-shopping-guide.md for dimensional weight and flat-rate guidance.CreateShipment per profile on the same route.references/carrier-guide.md for carrier-specific weight limits and surcharges.CreateShipment for the route.rates array by provider.ListShipments and ListTransactions to get past activity.GetTrack 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:
ListCarrierAccounts -> CreateShipment (per destination) -> read rates arrays -> write report
Carrier comparison:
CreateShipment -> group rates by provider -> summarize
Historical review:
ListShipments + ListTransactions -> cross-reference -> GetTrack (sample) -> write report
The Shippo MCP is hosted at https://mcp.shippo.com. It is OAuth-only and auto-updates server-side, so there is nothing to install or upgrade on your side. This skill covers what stays your responsibility: API version awareness, webhook payload versioning, and troubleshooting the hosted session.
The current Shippo API version is 2018-02-08. Shippo uses a single long-lived API version, and the hosted server manages it for you server-side. You do not set the Shippo-API-Version header yourself when going through the hosted MCP.
What backward-compatibility means in practice:
Shippo API changes are tracked in the API changelog. As of 2026-06, no recent breaking changes affect the workflows covered by this skill set.
Webhook events can include new fields without bumping the API version. To handle them gracefully:
track_updated, transaction_created, transaction_updated, etc.).Shippo-Signature header per webhook docs.401 or 403 errorsThe OAuth session has expired or is not authorized. Re-authorize the Shippo OAuth session: in Claude Code, run /mcp and sign in again.
The hosted server auto-updates, so the tool catalog can shift without any action on your side. Re-list the current tools via shippo_list_tools to see what is available now.
Most likely the object does not exist on the authorized account, or it belongs to a different account. Confirm you are signed in to the account that owns the object (re-authorize via /mcp if needed).
Before making a change to a production integration:
shippo_list_tools after an update to catch renamed or added operations.Turn a single shipment identifier into a complete, classified, well-structured support package for the Shippo support team. The agent classifies the issue, gathers every relevant fact from the Shippo MCP (running issue-type-specific lookups, not just the lost-package set), computes the triage timeline, and emits two things:
This dual output is the point: completeness and correct routing are what kill the back-and-forth.
Audience: Shippo support agents. Output uses Shippo terminology, object IDs, and an internal routing tag. It is not customer-facing copy.
Use this skill when someone wants to escalate or document a shipping problem and asks for a support ticket / message to Shippo support, e.g. "package is stuck," "label was charged but never shipped," "why was I charged more than the rate I saw," "refund this label I never used," "where is this delivery," "the address looks wrong," "tracking updates aren't coming through," "can't get rates from this carrier." It produces text + JSON to copy and paste; it does not open a Jira ticket or send Slack/email itself.
Pick exactly one canonical issue type from the customer's description. The issue type drives both the routing tag and which extra lookups you run in Step 4. If the wording is ambiguous, ask one clarifying question before building.
| Issue type (canonical) | Triggers / signals | Routing tag |
|---|---|---|
lost_or_delayed | stuck, late, no movement, "where is my package", lost | queue:tracking-ops |
unused_label_refund | "never shipped", "refund this label", bought-but-unused | queue:billing-refunds |
billing_adjustment | "charged more than the rate", surcharge, reweigh, dim-weight, address-correction fee | queue:billing-adjustments |
address_exception | undeliverable, returned to sender, bad/invalid address, address correction | queue:address-exceptions |
customs_international | customs hold, duties/taxes, missing HS code, commercial invoice, international | queue:customs-intl |
carrier_account | "can't get rates from ", connection failed, registration pending | queue:carrier-onboarding |
tracking_webhook | "tracking updates aren't coming through", webhook not firing | queue:integrations |
other | anything that doesn't fit above | queue:general-triage |
The routing tags above are placeholders for Shippo's real support queue names. Confirm the actual queue/label taxonomy and update this table once. The skill's value is producing a consistent, machine-parseable tag; the exact strings should match your ticketing system.
The user may start from any one of these. Ask which one they have if it is ambiguous; do not guess an ID type.
| Input | What it anchors |
|---|---|
| Tracking number + carrier | Drives GetTrack directly. Best for delivery/lost-package issues. |
| Transaction (label) object ID | Cleanest anchor: label creation time + tracking number + the rate/shipment link, all derivable. |
| Shipment object ID | Gives from/to addresses, requested shipment_date, and rates; tracking number comes from the purchased transaction. |
Resolving a tracking number to its label. First detect the carrier and map it to the Shippo carrier token (see the note below), then call
GetTrack. When the label was purchased through Shippo, theGetTrackresponse carries the transactionobject_id; use that withGetTransactionto pull the label and billing facts. If the label was not bought through Shippo (no transaction comes back), there is nothing to resolve: build the ticket fromGetTrackplus whatever the user supplied and mark the label fields "Not available."ListTransactionshas no server-sidetracking_numberfilter, so paging it to match by hand is a rarely-useful last resort, not the primary path.
Carrier token:
GetTrackexpects a Shippo carrier token, not a display name, e.g.usps,ups,fedex,dhl_express,dhl_ecommerce,canada_post. If you only have a display name (often from a rate'sprovider), map it to the token. If unsure, ask the user for the carrier.
Discover/confirm with shippo_list_tools and shippo_describe_tool; execute
read-only lookups with shippo_read_execute_tool. Everything this skill needs
is a read operation; never call a write tool (e.g. CreateRefund) from
this skill; the ticket only documents and recommends.
Core reads (all issue types):
GetTransaction: label creation time (object_created), tracking_number, status, rate reference, eta, metadata (order/internal reference)GetShipment: address_from, address_to, requested shipment_date, parcels, rates, customs_declaration, extra (added services + references), messagesGetTrack: current tracking_status, full tracking_history[], eta, and (for Shippo-purchased labels) the transaction object referenceIssue-type-specific reads (Step 4):
GetRate: purchased amount, currency, provider, servicelevel, estimated_days (billing)GetParcel: declared length/width/height, distance_unit, weight, mass_unit (billing)ListRefunds / GetRefund: existing refund object + status (refund)ValidateAddress / ValidateAddressByID: is_valid, messages, residential flag (address)GetCustomsDeclaration / GetCustomsItem: contents_type, incoterm, eel_pfc, per-item tariff_number (HS code), value_amount, origin_country (customs)ListCarrierAccounts / GetCarrierAccount / GetCarrierRegistrationStatus: active, registration status (carrier-account)listWebhooks / getWebhook: url, event, active (webhook)Always work toward having the four core objects: transaction, shipment, addresses, and tracking. Stop early only when the issue genuinely needs nothing more (e.g. a pure tracking-status question with no label on file).
GetTransaction. Read object_created (label creation
time), tracking_number, tracking_url_provider, status, and the rate
reference. Inspect for a shipment reference to get the shipment ID.GetShipment. Read address_from, address_to,
shipment_date (the requested ship date), parcels, rates,
customs_declaration. Find the purchased rate/transaction for the tracking #.GetTrack. For a Shippo-purchased label the response carries the transaction
object_id; follow it with GetTransaction to get the billing/label facts.Pull the shipment (GetShipment) for address_from, address_to,
shipment_date if not already loaded, and tracking (GetTrack with carrier
token + tracking number) for tracking_status, tracking_history[], and eta.
From each address object capture only its
object_idand coarse geography (city,state,zip,country) for the ticket, notname,street1, orstreet2(see PII minimization in guardrails).
tracking_history event representing
physical acceptance by the carrier (the first TRANSIT/DELIVERED-class scan,
or the carrier's "accepted/picked up" event). Pre-transit / "label created" /
"shipment info received" pseudo-events do not count; call those out
separately if present.extra block (added services such as signature_confirmation, insurance,
Saturday delivery, QR-code labels) and the customer's own order / internal
reference number. That reference can live in two places depending on the
integration: the transaction's metadata field (the documented home for order
numbers) and/or the shipment extra reference fields. Capture it from wherever
it actually appears, so the agent can tie the ticket back to the order without
searching on an order number. The extra schema is nuanced and
carrier/service-dependent, so read the actual response fields rather than
assuming names: the label-purchase skill documents the common added-service
options (signature, insurance, Saturday delivery) and
shippo/references/carrier-guide.md covers per-carrier availability. Surface
only what is actually present; omit the rest.messages noise: a shipment's messages array often carries routine
"carrier doesn't support option" / "out of service area" entries. These are
informational. Only surface messages tied to a carrier that actually appears
in rates.After the core facts, run only the lookups for the classified issue type and fill the matching section of the output. Skip branches that don't apply.
lost_or_delayed: no extra reads; the core timeline carries it. Emphasize
"last scan → now" and "overdue vs ETA."unused_label_refund: Was the label ever scanned? Re-check GetTrack: if
there is a real carrier scan, the label is used (not eligible as an unused
refund). Say so. Compute label age from object_created to now. Call
ListRefunds (and GetRefund) to report any existing refund object + its
status. Do not assert a specific eligibility window from memory; state
the facts (used/unused, age, existing refund) and let the queue apply policy.billing_adjustment: GetRate for the purchased amount/currency;
GetParcel (or shipment parcels) for declared dims/weight; compare the
transaction's charged amount to the quoted rate. Flag the likely cause:
dimensional-weight reweigh (declared vs billed dims), address-correction
surcharge, or service upgrade. Report declared-vs-billed as the core evidence.
Note: the reweigh/adjustment amount and the carrier's billed dims may not be
exposed by these read ops; if so, record "Not available" rather than inferring.address_exception: run ValidateAddress/ValidateAddressByID on
address_to; report is_valid, any validation messages, and the
residential/commercial flag. Note whether validation was bypassed at purchase.customs_international: pull GetCustomsDeclaration + each
GetCustomsItem. Check completeness: contents_type, incoterm,
eel_pfc/AES exemption, and per item a tariff_number (HS code),
value_amount, and origin_country. Flag missing HS codes / values, the
usual cause of customs holds.carrier_account: ListCarrierAccounts, then GetCarrierAccount /
GetCarrierRegistrationStatus for the relevant carrier. Report active and
registration status; an incomplete registration is the usual "no rates" cause.tracking_webhook: listWebhooks + getWebhook. Report whether an
active webhook exists for the relevant track_updated/tracking event and the
configured url.These derived metrics pre-diagnose the issue so support doesn't have to:
shipment_date → first carrier scan: picked up on/near intent?State each as an absolute date/time and a duration (e.g. "Label created 2026-06-01 14:02 UTC; first scan 2026-06-05 09:11 UTC, a 3d 19h gap"). Use UTC and label it. In the JSON block, also emit each gap in whole hours.
Emit both blocks below, each as its own fenced block. Replace every <...>
placeholder; use "Not available" for anything you could not retrieve; never
invent values.
Provenance (required). Both blocks carry a generation stamp so support can tell at a glance that the ticket was machine-assembled, and so ticket quality can be tracked over time. Stamp:
shippo-support-ticket),Shippo MCP),Never alter or omit the stamp, and never present an auto-generated ticket as if it were hand-written.
After the blocks, add a short plain-language triage summary (1-3 sentences) naming the most likely problem based on the classification + timeline, and list any data you could not retrieve.
Subject: [<issue_type>] <one-line summary>, tracking <tracking_number>
ROUTING
Issue type: <canonical issue type>
Routing tag: <queue:...>
Confidence: <high | medium | low; note if classified from sparse info>
ISSUE
Reported by: <customer name / email, if known>
Summary: <2-3 sentence description in plain language>
SHIPMENT
Shipment ID: <shipment object_id>
Transaction ID: <transaction object_id>
Carrier: <carrier display name> (<carrier token>)
Service level: <servicelevel name>
Tracking #: <tracking_number>
Tracking URL: <tracking_url_provider>
Parcel: <declared dimensions + weight, if available>
References: <order/internal ref from transaction metadata or shipment extra, else "none">
Added services: <signature / insurance / QR code / etc. from extra, else "none">
ADDRESSES (no street-level PII; run GetAddress on an ID for full details)
From address ID: <address_from object_id>
From region: <city> <state> <zip> <country>
To address ID: <address_to object_id>
To region: <city> <state> <zip> <country>
TIMELINE (all times UTC)
Label created: <object_created>
Requested ship date: <shipment_date>
First carrier scan: <status_date> @ <location> (<status>)
Last/most recent scan: <status_date> @ <location> (<status>)
Current status: <tracking_status>
Carrier ETA: <eta or "Not available">
Label created → first scan: <duration, e.g. 3d 19h>
Requested ship → first scan: <duration or note>
First scan → last scan: <duration>
Last scan → now: <duration>
Overdue vs ETA: <yes/no + by how much>
ISSUE-SPECIFIC FINDINGS
<Only the block for the classified issue type; examples:>
[unused_label_refund] Label used (scanned)? <yes/no>; Label age: <duration>;
Existing refund: <refund object_id + status or "none">
[billing_adjustment] Quoted rate: <amount> <ccy>; Charged: <amount> <ccy>;
Declared dims/wt: <...>; Likely cause: <reweigh/surcharge>
[address_exception] Address valid: <yes/no>; Validation messages: <...>;
Residential: <yes/no/unknown>
[customs_international] Contents type: <...>; Incoterm: <...>;
Items missing HS code/value: <list or "none">
[carrier_account] Carrier: <...>; Active: <yes/no>; Registration: <status>
[tracking_webhook] Active webhook for tracking events: <yes/no>; URL: <...>
TRACKING HISTORY (most recent first)
<status_date> <status> <location> <substatus/text>
<... one line per scan ...>
WHAT WE NEED FROM SUPPORT
<the specific ask: locate package / refund label / explain charge / fix
address / clear customs / complete carrier registration / fix webhook>
(Auto-generated by the "shippo-support-ticket" skill via the Shippo MCP on
<generation time UTC>. Facts collected automatically; verify before acting.)
{
"issue_type": "<canonical issue type>",
"routing_tag": "<queue:...>",
"classification_confidence": "<high|medium|low>",
"reported_by": "<email or name or null>",
"summary": "<one-line summary>",
"identifiers": {
"transaction_id": "<or null>",
"shipment_id": "<or null>",
"tracking_number": "<or null>",
"carrier_token": "<or null>",
"service_level": "<or null>",
"order_reference": "<order/internal ref from transaction metadata or shipment extra, or null>"
},
"shipment_extra": {
"<only the added-service `extra` fields actually present; e.g. signature_confirmation, insurance, qr_code>": ""
},
"addresses": {
"from": { "address_id": "<or null>", "city": "", "state": "", "zip": "", "country": "" },
"to": { "address_id": "<or null>", "city": "", "state": "", "zip": "", "country": "" }
},
"timeline_utc": {
"label_created": "<ISO8601 or null>",
"requested_ship_date": "<ISO8601 or null>",
"first_carrier_scan": "<ISO8601 or null>",
"last_scan": "<ISO8601 or null>",
"carrier_eta": "<ISO8601 or null>",
"current_status": "<or null>"
},
"gaps_hours": {
"label_to_first_scan": "<int or null>",
"requested_ship_to_first_scan": "<int or null>",
"first_to_last_scan": "<int or null>",
"last_scan_to_now": "<int or null>",
"overdue_vs_eta": "<int or null>"
},
"issue_findings": {
"<keys depend on issue_type; e.g. label_used, label_age_hours, existing_refund_status, quoted_amount, charged_amount, declared_dims, address_is_valid, items_missing_hs_code, carrier_active, registration_status, webhook_active>": ""
},
"requested_action": "<the specific ask>",
"data_gaps": ["<fields that could not be retrieved>"],
"generated_by": {
"skill": "shippo-support-ticket",
"source": "shippo-mcp",
"generated_at": "<ISO8601 UTC>"
}
}
write operations. Recommend a refund;
don't issue one.ListTransactions as a fallback: list the
candidates and ask the user to pick before building the ticket.low and say why, so the queue knows to sanity-check the tag.street1/street2) in the ticket, not in the human block
and not in the JSON. Reference the address_from / address_to object_ids
instead; support can run GetAddress on an ID to retrieve full details and
replicate the issue only when they actually need to. Coarse geography
(city, state/province, ZIP/postal, country) is retained, since support
needs it for zone/routing triage. Tracking-history location values
(typically city/state) are fine. Don't pull in unrelated shipments, and don't
include API tokens or raw object dumps beyond what the templates ask for.null (JSON)."10" not 10).carrier-accounts-list.https://mcp.shippo.com, authenticated by your per-user Shippo OAuth token. The server forwards each call to api.goshippo.com on your behalf. Nothing runs or is stored locally.