> ## Documentation Index
> Fetch the complete documentation index at: https://docs.goshippo.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Shippo is a multi-carrier shipping API. For agent integrations that execute shipping operations (rates, labels, tracking, address validation, customs), connect the hosted Shippo MCP server at https://mcp.shippo.com (per-user OAuth; setup at /guides/mcp-server). To search and read this documentation from an agent, a docs search MCP is available at https://docs.goshippo.com/mcp. Shipping workflow knowledge (agent skills and a knowledge pack) is published at https://github.com/goshippo/ai. For REST integrations start at /guides/api-quickstart; test mode uses shippo_test_ API keys.

# Test an OAuth integration with a JWT

> Use a JWT to verify which OAuth user you are acting as, put a carrier into test mode, and create a test rate and test label without spending money.

Once your platform completes the [OAuth flow](/o-auth-integrations/o-auth) for a user, you hold a Bearer `access_token` for that user's Shippo account. This guide walks through testing that connection end to end: exchange the Bearer token for a JWT, confirm which user you are acting as, put USPS into test mode, and buy a test label.

Every request on this page runs against the live `api.goshippo.com` host. Nothing here charges the connected account, provided you follow the test-rate check in [Create a test rate](#create-a-test-rate).

## Before you begin

You need:

* OAuth credentials for your platform (`client_id` and `client_secret`). See [OAuth](/o-auth-integrations/o-auth) to request them.
* A completed OAuth flow for a test user, which gives you that user's Bearer `access_token`.
* A Shippo account you are comfortable testing against. Use a dedicated test account rather than a production one — see [Sandbox testing](/partner-integration/sandbox-testing).

<Warning>
  **Use a dedicated test account**

  A Shippo OAuth `access_token` grants full API access to the account that authorized it and never expires. Do not run this walkthrough against an account you use for production shipping.
</Warning>

## Exchange the Bearer token for a JWT

Call `embedded/authz` with the user's Bearer token to mint a JWT. The `scope` field is required.

```shell Mint a JWT request theme={null}
curl https://api.goshippo.com/embedded/authz \
    --header 'Authorization: Bearer <OAUTH_BEARER_TOKEN>' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "scope": "embedded:carriers"
    }'
```

```json Mint a JWT response theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3QiOnsidXNlcl9pZCI6MTIzNDU2N30sImV4cCI6MTcwMDAwMDAwMCwiaXNfdGVzdCI6dHJ1ZSwib2F1dGhfYXBwX2lkIjoxMDAwMSwib3JpZ2luYWxfYXV0aF9zdHJhdGVneSI6IkpXVEF1dGgiLCJwYXJ0bmVyX25hbWUiOiJleGFtcGxlLXBhcnRuZXIiLCJwbGF0Zm9ybV9pZCI6MTAwMCwidXNlcl9pZCI6MTIzNDU2NywidXNlcl9vYmplY3RfaWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJ1c2VybmFtZSI6InNoaXBwb3RsZUBzaGlwcG8uY29tIn0.EXAMPLE_SIGNATURE_NOT_VALID",
  "expiresIn": 1700000000
}
```

`token` is the JWT. `expiresIn` is a unix timestamp marking when it expires — tokens are valid for 12 hours. Send the JWT on every subsequent request as `Authorization: JWT <JWT_TOKEN>`.

<Info>
  **note**

  `JWT` is the only accepted prefix for this token. `Authorization: Bearer <JWT_TOKEN>` and `Authorization: ShippoToken <JWT_TOKEN>` both return `401`.
</Info>

For the full set of ways to mint a JWT, including white label and eCommerce integrations, see [Authentication using JWT](/guides/authentication-using-jwt).

## Confirm which user you are acting as

Before you create anything, confirm the JWT resolves to the OAuth user you expect. This is the fastest way to catch a token that belongs to the wrong connected account.

```shell Check the account request theme={null}
curl https://api.goshippo.com/shippo-accounts \
    --header 'Authorization: JWT <JWT_TOKEN>'
```

```json Check the account response theme={null}
{
  "next": "",
  "previous": "",
  "results": [
    {
      "object_id": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
      "object_created": "2026-01-14T18:22:04.113Z",
      "object_updated": "2026-01-14T18:22:04.113Z",
      "email": "shippotle@shippo.com",
      "first_name": "Shwan",
      "last_name": "Ippotle"
    }
  ]
}
```

The JWT also carries the user's identity in its own claims. The payload is base64url-encoded JSON, so you can decode it without a Shippo API call — useful for asserting in your test suite that you connected the right account:

```json Decoded JWT payload theme={null}
{
  "act": { "user_id": 1234567 },
  "exp": 1700000000,
  "is_test": true,
  "oauth_app_id": 10001,
  "original_auth_strategy": "JWTAuth",
  "partner_name": "example-partner",
  "platform_id": 1000,
  "user_id": 1234567,
  "user_object_id": "00000000-0000-0000-0000-000000000000",
  "username": "shippotle@shippo.com"
}
```

<Warning>
  **Decode, do not trust**

  Decoding a JWT payload does not verify its signature. Read these claims for debugging and test assertions only, never as an authorization decision in your own application.
</Warning>

## Put USPS into test mode

Shippo provisions a test USPS carrier account alongside the live one. List the connected account's carrier accounts and find the USPS entry with `"test": true`.

```shell List carrier accounts request theme={null}
curl 'https://api.goshippo.com/carrier_accounts?results=50' \
    --header 'Authorization: JWT <JWT_TOKEN>'
```

```json List carrier accounts response theme={null}
{
  "next": "",
  "previous": "",
  "results": [
    {
      "object_id": "9f8e7d6c5b4a39281706f5e4d3c2b1a0",
      "carrier": "usps",
      "account_id": "********",
      "active": true,
      "test": false
    },
    {
      "object_id": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
      "carrier": "usps",
      "account_id": "shippo_usps_account",
      "active": false,
      "test": true
    }
  ]
}
```

Activate the test account with a `PUT`. You must include `test` in the body alongside `active` — omitting it returns `400 {"test": ["This field is required."]}`.

```shell Activate test mode request theme={null}
curl -X PUT https://api.goshippo.com/carrier_accounts/1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d \
    --header 'Authorization: JWT <JWT_TOKEN>' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "active": true,
        "test": true
    }'
```

```json Activate test mode response theme={null}
{
  "object_id": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
  "carrier": "usps",
  "account_id": "shippo_usps_account",
  "active": true,
  "test": true
}
```

## Create a test rate

Create a shipment exactly as you would in production. The JWT authenticates the request on behalf of the OAuth user.

```shell Create shipment request theme={null}
curl https://api.goshippo.com/shipments \
    --header 'Authorization: JWT <JWT_TOKEN>' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "address_from": {
            "name": "Shwan Ippotle",
            "street1": "215 Clayton St.",
            "city": "San Francisco",
            "state": "CA",
            "zip": "94117",
            "country": "US",
            "phone": "+1 555 341 9393",
            "email": "shippotle@shippo.com"
        },
        "address_to": {
            "name": "Mr Hippo",
            "street1": "965 Mission St",
            "street2": "Ste 201",
            "city": "San Francisco",
            "state": "CA",
            "zip": "94103",
            "country": "US",
            "phone": "+1 555 341 9393",
            "email": "mrhippo@shippo.com"
        },
        "parcels": [{
            "length": "5",
            "width": "5",
            "height": "5",
            "distance_unit": "in",
            "weight": "2",
            "mass_unit": "lb"
        }],
        "async": false
    }'
```

<Warning>
  **Test mode returns live rates too**

  Activating the test carrier account does not switch the account into test mode wholesale. The shipment returns rates from **every** active carrier account, live and test together — often the same service at the same price, distinguishable only by the rate's `test` field.

  Before you purchase, select your rate by `"test": true` and by the `carrier_account` of your test account. Purchasing a rate with `"test": false` buys a real label and charges the connected account.
</Warning>

```json Create shipment response (rates excerpt) theme={null}
{
  "object_id": "4c1b2a3d5e6f7089abcdef0123456789",
  "status": "SUCCESS",
  "rates": [
    {
      "object_id": "5d2c3b4a6f708192bcdef01234567890",
      "provider": "USPS",
      "servicelevel": { "name": "Ground Advantage" },
      "amount": "5.68",
      "carrier_account": "9f8e7d6c5b4a39281706f5e4d3c2b1a0",
      "test": false
    },
    {
      "object_id": "7e6d5c4b3a291807f6e5d4c3b2a19087",
      "provider": "USPS",
      "servicelevel": { "name": "Ground Advantage" },
      "amount": "5.68",
      "carrier_account": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
      "test": true
    }
  ]
}
```

Both entries are USPS Ground Advantage at the same price. Only the second one is a test rate.

## Create a test transaction

Purchase the test rate to produce a test label. Guard the selection in code rather than pasting a rate ID by hand:

```javascript Select and purchase a test rate theme={null}
const testRate = shipment.rates.find(
  (rate) => rate.test === true && rate.carrier_account === TEST_CARRIER_ACCOUNT_ID
);

if (!testRate) {
  throw new Error("No test rate returned — refusing to purchase a live label.");
}

const response = await fetch("https://api.goshippo.com/transactions", {
  method: "POST",
  headers: {
    Authorization: `JWT ${jwtToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    rate: testRate.object_id,
    label_file_type: "PDF_4x6",
    async: false,
  }),
});
```

```json Create transaction response theme={null}
{
  "object_id": "0f1e2d3c4b5a69788796a5b4c3d2e1f0",
  "status": "SUCCESS",
  "test": true,
  "rate": "7e6d5c4b3a291807f6e5d4c3b2a19087",
  "tracking_number": "9405500000000000000000",
  "tracking_status": "UNKNOWN",
  "label_url": "https://deliver.goshippo.com/0f1e2d3c4b5a69788796a5b4c3d2e1f0.pdf",
  "messages": []
}
```

Confirm `"test": true` on the response. A test transaction returns a real, downloadable label PDF and a tracking number, but is not a real shipment and is not billed.

## Clean up

Test rates keep appearing in every rate response for the connected account until you deactivate the test carrier account. When you finish testing, turn it back off:

```shell Deactivate test mode request theme={null}
curl -X PUT https://api.goshippo.com/carrier_accounts/1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d \
    --header 'Authorization: JWT <JWT_TOKEN>' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "active": false,
        "test": true
    }'
```

## Troubleshooting

| Symptom                                                                  | Cause                                                                                                                                               |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `307` with no response body from `embedded/authz/`                       | The canonical path has no trailing slash. Use `https://api.goshippo.com/embedded/authz`, or make sure your HTTP client follows redirects on `POST`. |
| `401 {"detail": "Invalid OAuth token header. No credentials provided."}` | The `Authorization` header is missing or empty. Check that your Bearer token was actually interpolated into the request.                            |
| `401` when using a freshly minted JWT                                    | Wrong prefix. The header must be `Authorization: JWT <JWT_TOKEN>`, not `Bearer` or `ShippoToken`.                                                   |
| `400 {"message": "The scope field is required."}`                        | `embedded/authz` requires a `scope` in the request body.                                                                                            |
| `400 {"test": ["This field is required."]}` on carrier account update    | Include `test` alongside `active` in the `PUT` body.                                                                                                |
| A label you expected to be free appears on an invoice                    | The purchased rate had `"test": false`. Rate responses mix live and test rates; filter on the rate's `test` field before purchasing.                |


## Related topics

- [OAuth](/o-auth-integrations/o-auth.md)
- [Authentication using JWT](/guides/authentication-using-jwt.md)
- [Create a JWT](/api-reference/embedded-authorization/create-a-jwt.md)
