# Authentication and API keys

Every request to `https://shipjay.com/api/v1` carries an API key as a bearer token:

```http
Authorization: Bearer sj_...
```

A key looks like `sj_` followed by a 12 character prefix, an underscore and a long secret. The prefix identifies the key in the app; the secret is shown once when you create the key. Only a hash of the secret is stored, so Ship Jay cannot show you a key again. If you lose one, create a new key and revoke the old one.

A missing, malformed, unknown, revoked or expired key all return the same `401` with the code `unauthenticated`, so a caller cannot learn which part was wrong.

## Create a key

Open Settings, then Developers in the app and choose to create a key. You set:

- **Name.** Anything that helps you recognize the key later, such as the system that uses it.
- **Permissions.** At least one. See the table below.
- **Per label limit.** The most a single purchase made with this key may cost.
- **Per day limit.** The most this key may spend in a day. The day follows your account time zone.
- **Expiry date.** Optional. After this moment the key stops working.
- **IP allowlist.** Optional. Up to 20 entries, each an IP address or an IPv4 range such as `203.0.113.0/24`.

An account can have at most 50 active keys.

## Permissions

Each endpoint needs one permission. A key without it gets `403` with the code `forbidden`. `GET /account` needs none and works with any valid key.

| Permission | What it allows |
| --- | --- |
| `shipments:read` | Read shipments, labels and tracking |
| `shipments:write` | Get rates and create shipments |
| `shipments:buy` | Buy labels (spends wallet money) |
| `refunds:write` | Request label refunds |
| `wallet:read` | Read the wallet balance and ledger |
| `returns:read` | Read returns |
| `returns:write` | Approve, reject and receive returns |
| `rules:read` | Read shipping rules |
| `rules:write` | Create and change shipping rules |
| `events:read` | Read the event feed |
| `webhooks:manage` | Manage webhook endpoints |

**Caution:** `shipments:buy` spends wallet money and is separate from `shipments:write` on purpose. Give it only to keys that must buy labels, and always set spending limits on those keys.

## Check what a key can do

```bash
curl https://shipjay.com/api/v1/account \
  -H "Authorization: Bearer $SHIPJAY_KEY"
```
```js
const res = await fetch("https://shipjay.com/api/v1/account", {
  headers: { Authorization: `Bearer ${process.env.SHIPJAY_KEY}` },
});
console.log(await res.json());
```
```python
import os, requests

res = requests.get(
    "https://shipjay.com/api/v1/account",
    headers={"Authorization": f"Bearer {os.environ['SHIPJAY_KEY']}"},
    timeout=30,
)
print(res.json())
```
The response lists the key's `scopes`. The MCP server uses the same list to decide which tools to offer.

## Spending limits

Limits are checked inside the purchase, before any money moves. A purchase over the per label limit, or one that would take the key over its day total, fails with `402` and the code `spend_limit`. The `details` field has the limit in cents, and for the day limit also what the key has spent so far. Your account has its own daily limit and wallet balance, which apply in addition to the key limits.

## Expiry and IP allowlist

- A key past its expiry date returns `401 unauthenticated`.
- If a key has an allowlist, a request from any other address returns `403 forbidden`. The address is the one reported by the proxy in front of Ship Jay. If the deployment does not report one, a key with an allowlist cannot be used.

## Rotate a key

1. Create a new key with the same permissions and limits.
2. Deploy the new key to the systems that use it.
3. Check in the app that the old key's last used time stops moving.
4. Revoke the old key. Revoked keys stop working immediately.

## Keep keys safe

- Call the API from a server. It sends no CORS headers, so browsers cannot call it, and a key in front end code is a key anyone can copy.
- Store keys in a secrets manager or environment variable, never in source control.
- Use one key per system so you can revoke one without breaking the others.
- Repeated requests with bad keys from one IP address are blocked for 15 minutes. See [Rate limits and abuse protection](/docs/rate-limits).