Migrating to v2
v2 is not a compatibility layer over v1. It is a separate API registered under api/v2 with its own authentication, its own envelope and its own identifiers, so migrating is a rewrite of the transport layer rather than a base-URL swap. This page maps what you have onto what replaces it.
The v2 side of every claim here is documented in full in the v2 reference; this page only says which v2 route replaces which v1 route, and what changes around it.
The five changes that touch every call
1. Credentials move from the payload to headers
v1 authenticates with license_key and domain inside the request body or query string. v2 authenticates the integration with two headers and selects the carrier account separately:
X-Client-Id: {client_id}
X-Client-Secret: {client_secret}domain disappears entirely. license_key survives but changes job — it is no longer a credential, just the account selector, and it becomes optional when the account owns exactly one active license. See v2 Authentication and Licenses & license_key.
2. The envelope changes shape
| v1 | v2 | |
|---|---|---|
| Success | {data, message, success, warning} | {data} |
| Failure | the same four keys, success: false | {error: {code, message, status}} |
v2 errors carry a machine-readable code, which v1 has no equivalent of — v1 callers have to match on message text. See v2 Errors.
3. The HTTP status becomes trustworthy
This is the change most likely to break a port done carelessly. In v1 the success flag is authoritative and the status often is not: several failure paths return success: false with HTTP 200, and one returns a validation failure as HTTP 500. Code written against v1 therefore tends to ignore the status and branch on success.
In v2 the status is always the real one, and there is no success flag to read. A port that keeps checking body.success will treat every v2 response as a failure, because the key is absent.
4. Reads become GET, and resources get URLs
v1 is POST-for-everything with no path parameters: you identify a shipment by putting its shipping_code in a body. v2 addresses resources by URL — GET /shipments/{uuid} — which also makes responses cacheable, and v2 serves ETags to support that.
5. Identifiers change
v1 addresses a shipment by shipping_code. v2 addresses it by uuid and exposes the carrier tracking code as a separate field. Any stored shipping_code values need to be resolved to uuids, or looked up through the order.
Route mapping
Shipments
| v1 | v2 | Notes |
|---|---|---|
POST /shipping/open_new_order | POST /shipments | See idempotency below — the retry semantics differ materially. |
POST /shipping/get_order_details | GET /shipments/{uuid} | |
POST /shipping/get_shipping_status | GET /shipments/{uuid}/status | |
POST /shipping/{shipment}/status | GET /shipments/{uuid}/status | The two v1 status routes collapse into one v2 route. |
POST /shipping/change_order_status | POST /shipments/{uuid}/cancel | Closest equivalent. v1's endpoint is a general status change; v2 models cancellation explicitly rather than as a status write. |
POST /shipping/order | GET /orders/{order}/shipments |
Labels
| v1 | v2 | Notes |
|---|---|---|
POST /shipping/label | GET /shipments/{uuid}/label | |
POST /shipping/bulk-label | POST /shipments/labels | |
POST /shipping/order-label | GET /orders/{order}/shipments, then a label call per shipment | No single-call equivalent. |
Pickup points
| v1 | v2 | Notes |
|---|---|---|
POST /shipping/pickup_locations | GET /pickup-points | The v2 response is a flat list. v1 nests the array under data.locations.spots.spot_detail; v2 does not. v2 also adds city filtering, limit, and nearest-first ranking by coordinates or a geocoded address. |
Account and licenses
| v1 | v2 | Notes |
|---|---|---|
GET /shipping/user-by-license | GET /account | |
GET /license/check_license | GET /licenses | v2 lists the licenses the credentials can act under; validating a key becomes "is it in that list". |
GET /license/activate_license | — | No equivalent. Binding a license to a domain is an account-provisioning step, not an API operation, in v2. |
No v2 equivalent
| v1 | Why |
|---|---|
GET /shipping/same_day_delivery_quote | The express/same-day endpoints were never carried across. If you depend on them, stay on v1 for that call and raise it with the ShipOS team. |
GET /shipping/create_shipping_promise | as above |
GET /shipping/get_express_delivery_enabled | as above |
GET /shipping/get_express_delivery_status | as above |
GET /get_app_user | First-party recipient-app surface, not a merchant integration point. |
POST /create_app_user | as above |
GET /shipping/phone_number | as above |
Retry safety changes meaning
v1 protects open_new_order implicitly. A successful creation is cached for an hour under license_key + order_id, so a retry inside that window replays the first response instead of creating a second shipment. You get that whether you asked for it or not — and you cannot opt out, which is why creating two different shipment types for the same order inside the hour returns the first one again. That sharp edge is documented on Shipments.
v2 makes it explicit: you send an Idempotency-Key header of your own choosing, scoped to the request you actually made. Retries are safe because you said what counts as the same operation, not because the server guessed from order_id. See v2 Idempotency.
Practically: if your v1 code relies on the implicit cache to deduplicate, you must start sending Idempotency-Key in v2, or you will create duplicates — real shipments, with real carrier charges.
What you gain
Capabilities with no v1 counterpart at all:
- Batches — queue up to 2,000 shipments, poll progress, inspect per-item failures, without holding a connection open. v1's only bulk primitive is a combined label URL.
- Webhooks — HMAC-signed lifecycle events with a delivery log and test pings, instead of polling status endpoints.
- Exports — asynchronous CSV extracts.
- Notifications — SMS settings, templates and logs.
- Reference data — the canonical shipping-status and service-type lists, rather than matching on strings.
- Tracking — a public tracking lookup needing no credentials.
- Health —
GET /ping, so you can check reachability without credentials.
Suggested order of work
- Get credentials and confirm them against
GET /account. - Port creation first —
POST /shipmentswith anIdempotency-Key— and run it alongside v1 on a small share of traffic. - Port reads: status, shipment fetch, order shipments.
- Port labels.
- Replace status polling with webhooks, which is usually the largest single simplification.
- Keep any express/same-day calls on v1 until an equivalent exists.
A worked end-to-end walkthrough is in Create your first shipment.