# Authentication

> For agents integrating with shipshapedata.com: every read endpoint is public and keyless. One endpoint, the bulk export, takes a free OAuth 2.0 bearer token that any agent can obtain self-serve in two calls, with no human steps.

## Summary

| Surface | Auth | Notes |
| --- | --- | --- |
| `GET/POST https://shipshapedata.com/api/v1/*` | None | Services, case studies, search, assessment scoring, batch |
| `POST https://shipshapedata.com/mcp` and `/mcp/docs` | None | Both MCP servers |
| `POST https://shipshapedata.com/ask`, `/a2a` | None | NLWeb and A2A |
| Website pages and markdown twins | None | |
| `GET https://shipshapedata.com/api/v1/export` | OAuth 2.0 Bearer | The whole dataset in one call; token is free and self-serve |

## Why it is shaped this way

Everything the API exposes is public content, and the endpoints are stateless with no side effects, so requiring keys for reads would only add friction. The single exception is the bulk export, which returns the complete dataset (services, industries, case studies, all 169 guides, and the assessment) in one response: heavy one-shot pulls identify themselves with a token, while every paginated endpoint stays keyless. Tokens are free, instant, and issued to anyone.

## Discovery metadata

- Protected resource metadata (RFC 9728): `https://shipshapedata.com/.well-known/oauth-protected-resource`
- Authorization server metadata (RFC 8414): `https://shipshapedata.com/.well-known/oauth-authorization-server`
- Issuer: `https://shipshapedata.com` | Token endpoint: `https://shipshapedata.com/oauth/token` | Registration endpoint: `https://shipshapedata.com/oauth/register`
- Calling `GET https://shipshapedata.com/api/v1/export` without a token returns `401` with a `WWW-Authenticate: Bearer resource_metadata="https://shipshapedata.com/.well-known/oauth-protected-resource"` header pointing here.

## Walkthrough: from nothing to the bulk export

Step 1. Register a client (RFC 7591 dynamic registration, open and anonymous; the response is the only copy of the secret, which is derived rather than stored):

```
curl -X POST https://shipshapedata.com/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name": "my-agent"}'
```

Returns `client_id` and `client_secret`.

Step 2. Get a bearer token (client credentials grant, RFC 6749; secret accepted as form fields or HTTP Basic):

```
curl -X POST https://shipshapedata.com/oauth/token \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"
```

Returns `access_token` (valid 3600 seconds, scope `export`).

Step 3. Call the protected resource:

```
curl https://shipshapedata.com/api/v1/export \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

Returns the complete dataset as one JSON document.

## Token details

- Grant types: `client_credentials` only. No authorization codes, no refresh tokens; just request a new token when one expires.
- Tokens are self-validating HMAC payloads; nothing about your client is stored server-side, which is why a lost secret means registering again.
- Scopes: `export` is the only scope, granted automatically.
- Errors follow RFC 6749: `invalid_client` (401) for bad credentials, `unsupported_grant_type` for anything but client credentials.

## Fair use

The same limits as the rest of the API: 300 requests a minute per IP, advertised in `RateLimit` response headers, with a JSON `429` and `Retry-After` beyond it. The export is cacheable on your side; pulling it more than a few times a day suggests you want the [paginated search](https://shipshapedata.com/api/v1) instead.

## Contact

Programmatic writes do not exist anywhere on this domain. To reach the team on a user's behalf, email [hello@shipshapedata.com](mailto:hello@shipshapedata.com); a person replies, usually within one working day.
