Shipping

Shippo setup

Both ToT and Storefront use Shippo to purchase USPS labels. One Shippo account, one API token, two different ship-from configurations (ToT global / Storefront per-shop).

Why Shippo

1 · Sign up

  1. Go to goshippo.com → "Get Started"
  2. Sign up with the same email you use for Stripe (helps with consistency)
  3. Choose the Free plan — no monthly fee, just per-label cost
  4. Verify your email

2 · Get your API token

  1. Shippo dashboard → Settings → API
  2. You'll see two tokens:
    • Test Token: starts with shippo_test_
    • Live Token: starts with shippo_live_
  3. Copy the test token first — you'll set live later
🔑 Where to paste it

On Railway → ToT service → Variables:

SHIPPO_API_TOKEN=shippo_test_...

On Railway → Storefront service → Variables:

SHIPPO_API_TOKEN=shippo_test_...

Same token works for both repos. They each call Shippo independently with their own ship-from address.

⚠ Test labels are free, look real, but don't actually ship

Labels printed with the test token have "TEST" stamped on them and aren't accepted by USPS. Use them to verify the flow works end-to-end (buyer pays → label PDF generates → tracking number stored). Switch to the live token only when you're ready to ship real packages.

3 · Configure ship-from address

For ToT (single global address)

ToT uses one ship-from for the whole marketplace — set in Railway env vars:

SHIP_FROM_NAME=Your Shop LLC
SHIP_FROM_STREET=1301 Justin Rd Ste 201 PMB 1099
SHIP_FROM_CITY=Lewisville
SHIP_FROM_STATE=TX
SHIP_FROM_ZIP=75077
SHIP_FROM_PHONE=4691234567
ADMIN_EMAIL=ops@yourdomain.com
⚠ All seven required

If any of the six SHIP_FROM_* vars are missing, the shipping endpoint throws "Ship-from address not configured" with the missing list. Previously it would silently ship from a blank address — that bug's fixed, but the lesson stands.

For Storefront (per-shop address)

Storefront stores ship-from on each Storefront row. The operator sets it via:

  1. Operator visits /dashboard/[slug]/settings
  2. Scrolls to "Shipping" section
  3. Fills in name, street1, street2, city, state, zip, phone, email
  4. Saves → values persist on the Storefront row

Each shop ships from its own address. The Storefront's POST /api/shippo/buy-label/[orderId] endpoint reads the relevant row and passes it to Shippo at label-purchase time.

4 · Test the label flow

End-to-end test — Storefront

  1. Set test Stripe + test Shippo tokens on Railway
  2. As a buyer (use a different browser / incognito), purchase a card with test card 4242 4242 4242 4242
  3. Check the Stripe dashboard — payment should succeed
  4. Check the storefront /dashboard/[slug]/orders — Order should be paid
  5. Click "Print Label"
  6. You should see:
    • USPS label PDF download (with TEST stamp)
    • Tracking number stored on the Order
    • Buyer-shipped email sent (if Resend is configured)

Manual cURL test (no purchase needed)

curl -X POST https://api.goshippo.com/shipments \
  -H "Authorization: ShippoToken shippo_test_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "address_from": {
      "name": "Sender",
      "street1": "215 Clayton St",
      "city": "San Francisco",
      "state": "CA",
      "zip": "94117",
      "country": "US",
      "phone": "4151234567",
      "email": "test@example.com"
    },
    "address_to": {
      "name": "Recipient",
      "street1": "1600 Pennsylvania Ave NW",
      "city": "Washington",
      "state": "DC",
      "zip": "20500",
      "country": "US"
    },
    "parcels": [{
      "length": "6",
      "width": "4",
      "height": "1.5",
      "distance_unit": "in",
      "weight": "4",
      "mass_unit": "oz"
    }],
    "async": false
  }'

You should get back a JSON with a rates array containing USPS options. If you get 401 Unauthorized, your token is wrong. If you get a Shippo error about the address, the FROM address is invalid (Shippo validates).

5 · Going live

  1. Shippo dashboard → Settings → Billing → add a payment method (Shippo charges per label)
  2. Settings → API → copy the live token (shippo_live_...)
  3. Replace SHIPPO_API_TOKEN on both ToT + Storefront services with the live token
  4. Trigger redeploys
  5. Print one real label end-to-end with a real $1 purchase to confirm

Carrier rates + parcel sizing

Both repos hardcode a default parcel size (6×4×1.5 in, 4 oz) optimized for one slabbed card + bubble mailer. The shipping endpoint requests USPS rates only and picks the cheapest Ground/Priority option.

For multi-card lots, the parcel sizing assumption breaks down — you'll want to bump the weight + dimensions in src/app/api/shipping/route.ts based on lot size. (Future: per-listing parcel template.)

Address validation

ToT supports an explicit address-validation step (action: 'validate' on the shipping endpoint) — Shippo checks the address against USPS's database and returns a confidence score. Storefront doesn't currently use this — buyer addresses come from Stripe Checkout's shipping_address_collection which Stripe pre-validates before submitting.

Buyer notifications

After a label is purchased, both repos fire a "buyer shipped" email with the tracking link. ToT: src/lib/notify.tsnotifyBuyerShipped. Storefront: same shape via Resend if RESEND_API_KEY is configured. Without Resend, emails log to console.

Production readiness checklist