# AGENTS.md — Shipyu

> Instructions for AI agents integrating the Shipyu shipment tracking API.
> Human-facing docs live at https://shipyu.com/docs. The machine-readable spec is
> at https://shipyu.com/api/openapi (OpenAPI 3.1, no auth required).

## What this service does

Shipyu tracks parcels. You give it a tracking number; it identifies the
carrier, polls for scans, normalizes every carrier's vocabulary into one
status model, and pushes changes to your webhook endpoint. It covers 100+
carriers including USPS, UPS, FedEx, and DHL.

It does not print labels for you as its primary purpose, run a storefront,
or send customer emails. If the task is "tell me where this package is" or
"notify my system when this package moves", this is the right tool.

## Getting credentials

1. Sign up at https://shipyu.com/signup. Self-serve, instant.
2. A test key (`sk_shipyu_test_...`) is issued as soon as the email is
   verified. No credit card, no sales call, no approval queue.
3. Live keys (`sk_shipyu_live_...`) require a payment method on file.

Authenticate with `Authorization: Bearer <key>`.

**Test mode is free forever.** Prefer it for anything exploratory. Test-mode
requests never contact a carrier and never record a billable event, so you
can verify an entire integration — including webhook delivery and signature
verification — before any money is involved.

## Two ways in

### MCP (preferred if you are an agent with tool support)

```
claude mcp add shipyu --transport http https://shipyu.com/api/mcp \
  --header "Authorization: Bearer sk_shipyu_test_..."
```

Endpoint: `https://shipyu.com/api/mcp` (MCP Streamable HTTP, JSON only).
Tools: `create_tracker`, `get_tracker`, `delete_tracker`, `list_webhooks`,
`create_webhook`, `test_webhook`, `get_usage`.

### REST

Base URL: `https://shipyu.com/api/v1`

Create a tracker — omit `carrier` and it is detected from the number:

```
curl https://shipyu.com/api/v1/trackers \
  -H "Authorization: Bearer sk_shipyu_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tracker":{"tracking_code":"9400111899223033005436","carrier":"USPS"}}'
```

Read it back:

```
curl https://shipyu.com/api/v1/trackers/trk_abc123 \
  -H "Authorization: Bearer sk_shipyu_test_YOUR_KEY"
```

## Things that will trip you up

These are the behaviours worth knowing before you spend a call discovering
them:

- **There is no collection listing.** `GET /v1/trackers` returns 403 with
  code `listing_not_available`. Retrieve by id, or subscribe to webhooks.
  This is a tenant-isolation guarantee, not an oversight — do not retry it
  with pagination parameters, it will not start working.
- **Live mode without a payment method returns 402** with code
  `payment_method_required`. This is not a transient error. Do not retry;
  either switch to a test key or tell the user to add a card.
- **Do not poll for status changes.** Live trackers update themselves and
  push to your webhook. Polling `GET /trackers/{id}` in a loop burns your
  rate limit and tells you nothing a webhook would not have.
- **Creating a tracker is the billable event, not reading one.** Reads and
  webhook deliveries are free. Creating the same tracking number twice
  creates two trackers and bills twice — store the returned `trk_` id.

## Rate limits

100 requests/minute and 1,000 requests/hour, per API client — multiple keys
belonging to the same client share one bucket. Every response carries
`X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`
(Unix timestamp). Read them and back off before you are throttled rather
than after.

Exceeding a limit returns 429 with code `rate_limit_exceeded`.

## Errors

Errors are JSON: `{"error":{"type":..., "code":..., "message":...}}`.

Types: `authentication_error`, `invalid_request_error`, `forbidden_error`,
`rate_limit_error`, `billing_error`, `api_error`.

Retry `rate_limit_error` (after the reset) and `api_error` (with backoff).
Do not retry `authentication_error`, `invalid_request_error`, or
`forbidden_error` — the same request will fail identically.

## Webhooks

Register an endpoint, then verify every delivery before trusting it. The
signature header is `t=<unix-timestamp>,v1=<hex hmac-sha256>`, where the
signed payload is `${timestamp}.${raw-body}`:

```js
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, rawBody, header) {
  const m = header.match(/^t=(\d+),v1=([0-9a-f]{64})$/);
  if (!m) return false;
  const expected = createHmac("sha256", secret)
    .update(`${m[1]}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(m[2], "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

Verify against the **raw** body, not a re-serialized object — re-serializing
changes the bytes and the signature will not match.

Failed deliveries retry 5 times at 1 minute, 5 minutes, 30 minutes, 2 hours,
and 12 hours, each with ±20% jitter. Endpoints must be HTTPS and must not
resolve to private or unroutable address space.

## Deterministic test data

Test mode accepts ordinary tracking numbers and returns realistic timelines
from a fixed base date, so the same input always produces the same output.
Specific tracking codes trigger specific scenarios:

- `TEST_UNKNOWN_000` — unknown status
- `TEST_ERROR_000` — error status
- `TEST_EXCEPTION_000` — delivery exception

Use these to exercise your error handling without waiting for a real parcel
to go wrong.

## Pricing

$0.020 per tracker created, falling to $0.015 at volume across six
graduated brackets. No platform fee, no seat licences, no monthly quota.
Reads, webhook deliveries, and all test-mode calls are free. Full table at
https://shipyu.com/pricing.

## Canonical links

- Full API as plain text: https://shipyu.com/llms-full.txt
- OpenAPI 3.1 spec (no auth): https://shipyu.com/api/openapi
- Quickstart: https://shipyu.com/docs/quickstart
- MCP setup: https://shipyu.com/mcp
- Changelog: https://shipyu.com/changelog (RSS: https://shipyu.com/changelog/feed.xml)
- Status of what is implemented vs claimed: https://shipyu.com/security
