# SHIP auth.md

This file tells an AI agent how to get a credential for SHIP and how to use it. SHIP is an agentic engineering platform at https://letsship.ai.

SHIP supports agentic registration (Auth.md v0.6.0). You can register yourself, and the person you act for then confirms you in the SHIP console. An MCP client can sign the person in with OAuth. A person can also create a personal access token for you.

## Audience

This file is for agents, scripts and command-line tools that act for a SHIP user. A SHIP credential gives access to two services:

- The SHIP API at `https://api.letsship.ai/v1`
- The SHIP MCP server at `https://mcp.letsship.ai/mcp`

## Discovery

The issuer is `https://letsship.ai`. Read these documents before you register:

- Authorization server metadata: https://letsship.ai/.well-known/oauth-authorization-server. Its `agent_auth` block names the registration and claim endpoints.
- Protected resource metadata for the API: `https://api.letsship.ai/.well-known/oauth-protected-resource`
- Protected resource metadata for the MCP server: `https://mcp.letsship.ai/.well-known/oauth-protected-resource/mcp`

A `401` from the API has a `WWW-Authenticate` header. Its `resource_metadata` parameter points to the protected resource metadata.

## Supported methods

| Method | Use it when | Supported |
| --- | --- | --- |
| `anonymous` | You have no identity for your user yet | Yes |
| `service_auth` | You know the email address of your user | Yes |
| `identity_assertion` (ID-JAG) | Your agent provider is on the SHIP trust list | Yes |
| OAuth authorization code | You are an MCP client that can open a browser | Yes |
| Personal access token | A person creates a token for you in the console | Yes |

The trust list is in the metadata, at `agent_auth.identity_assertion.trusted_issuers`. If your provider is not on that list, use `service_auth` or `anonymous`.

## Step 1: Register

Send `POST https://api.letsship.ai/v1/agent/identity` with a JSON body. You can add `agent_name`, so the person can recognize you.

```json
{ "type": "service_auth", "login_hint": "ada@example.com", "agent_name": "Release notes agent" }
```

The response depends on the method:

- `anonymous` returns an `identity_assertion` and a `claim_token`. Until a person claims you, your token reaches only `/v1/me` and `/v1/scopes`.
- `service_auth` returns a `claim_token` and a `claim` block. Go to step 2.
- `identity_assertion` returns an `identity_assertion` when the person already linked your provider. The first time, it returns `401` with `interaction_required` and a `claim` block.

An ID-JAG must carry a verified email, a `client_id`, a fresh `jti` and an `auth_time`. If the login at your provider is older than one hour, SHIP returns `401` with `login_required`. Then sign the person in again at your provider.

## Step 2: Let the person claim you

For an anonymous registration, start the claim with `POST https://api.letsship.ai/v1/agent/identity/claim` and the email address of your user:

```json
{ "claim_token": "clm_...", "email": "ada@example.com" }
```

Show the person the `verification_uri` and the `user_code` from the `claim` or `claim_attempt` block. The person signs in, types the code and chooses what you can do. Only the account with that email address can claim you.

While you wait, send `POST https://api.letsship.ai/v1/oauth2/token` every `interval` seconds:

```
grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_...
```

- `authorization_pending`: the person did not finish yet. Wait and send again.
- `slow_down`: you sent too fast. Add five seconds to your interval.
- `expired_token`: the code expired after ten minutes, or five wrong codes locked it. Call the claim endpoint again for a new code.
- Success: you get an `access_token` and a new `identity_assertion`. After that, the claim token is not valid.

The person has 24 hours to claim you. After that, register again.

## Step 3: Get an access token

Send `POST https://api.letsship.ai/v1/oauth2/token` with your identity assertion:

```
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=eyJ...
```

An access token starts with `agt_` and lasts one hour. When it expires, send the same assertion again. An assertion from a claim lasts as long as the access that the person gave you. An assertion from an ID-JAG lasts one hour.

The token works for the API. For the MCP server, add `resource=https://mcp.letsship.ai/mcp`. Each service refuses a token that was made for the other one.

## Use the token

Send the token in the `Authorization` header as a bearer token. SHIP reads the token only from this header.

```bash
curl -H "Authorization: Bearer $SHIP_API_TOKEN" https://api.letsship.ai/v1/me
```

Call `GET https://api.letsship.ai/v1/me` first to check the token. The response gives the user, the organization, the role and the scopes of the token. Its `projectId` is the project of the token, or `null` for a token without a project. Before a claim, its `principalType` is `agent`.

For the MCP server, send the same header with each request:

```json
{
  "mcpServers": {
    "ship": {
      "url": "https://mcp.letsship.ai/mcp",
      "headers": { "Authorization": "Bearer pat_your_token_here" }
    }
  }
}
```

The `whoami` tool of the MCP server returns the same data as `GET /v1/me`. The `ship` CLI reads the token from the `SHIP_API_TOKEN` environment variable.

Keep every token, assertion and claim token secret. Do not write them to logs, commits, issues or pull requests.

## MCP clients: sign in with OAuth

An MCP client that supports OAuth, for example Claude Code, needs only the server URL:

```json
{ "mcpServers": { "ship": { "type": "http", "url": "https://mcp.letsship.ai/mcp" } } }
```

The first request gets a `401` with a `WWW-Authenticate` header that points to the metadata. Then sign the person in:

1. Register your client: send `POST https://api.letsship.ai/v1/oauth2/register` with your `redirect_uris`.
2. Open `https://letsship.ai/oauth/authorize` in the browser.
3. Send `response_type=code`, `client_id`, `redirect_uri`, `state` and `resource` in the query.
4. Add a PKCE `code_challenge` with `code_challenge_method=S256`.
5. Wait for the browser to return to your redirect URI with a `code`.
6. Send the code and your `code_verifier` to the token endpoint within one minute.

Instead of a registration, you can use the HTTPS URL of a Client ID Metadata Document as your `client_id`. A redirect URI must be `https`, `http` on `127.0.0.1` or `localhost` with any port, or a private-use scheme. The console shows the person the name of your app and the host of your redirect URI. The person then selects the organization, the access and the expiry.

Add `offline_access` to `scope` to get a refresh token. Each refresh gives you a new refresh token, and the old one stops. If you send an old refresh token again, SHIP revokes the sign-in. SHIP does not publish OpenID Connect discovery, because it does not issue ID tokens.

## A2A clients: sign in with OAuth

The A2A endpoint `https://mcp.letsship.ai/a2a` is its own OAuth resource. Use the same steps as an MCP client, and send `resource=https://mcp.letsship.ai/a2a`. A token for the MCP server or the API does not work on the A2A endpoint, and an A2A token does not work on them. The Agent Card at https://letsship.ai/.well-known/agent-card.json names the endpoints and the scopes. A personal access token also works on the A2A endpoint.

## Personal access tokens

A person can also create a token for you, which starts with `pat_`. If the user does not have a token, give them these steps:

1. Sign in to SHIP at https://letsship.ai/auth/signin.
2. Open Settings, then API tokens, at https://letsship.ai/settings/tokens.
3. Type a name that tells you where the agent uses the token.
4. Select an access level for each resource that the task needs.
5. Optional: select a project to limit the token to that project.
6. Select an expiry. The default is 90 days, and the maximum is 365 days.
7. Copy the token. The console shows the token one time only.

You cannot create a token yourself, because the API refuses a token request that comes from another token.

## Scopes

Ask for the smallest access that the task needs. For example, to start missions and read their results, ask for `missions:run` and `missions:read`. You never get more access than the person who claims you or creates your token. The full list of scopes, and the role that each scope needs, is at https://letsship.ai/docs/api/authentication.

If the person limits your access to one project, you can only reach the missions, runs, environments, artifacts and scheduled tasks of that project.

## Errors

- `401 Unauthorized`: the token is not in the header, or it is malformed, expired or revoked. Get a new token with your assertion. If that fails, register again.
- `403 Forbidden` with `missing-scope:<scope>` in the description: the token does not have that scope. Ask the user for that access. Do not send the same request again with the same token.
- `403 Forbidden` with the code `agent_not_claimed`: nobody claimed you yet. Go to step 2.
- `invalid_grant` from the token endpoint: your assertion expired, the claim replaced it, or the person revoked you. Register again.
- `429` with `rate_limited`: you registered too often from one address. Wait for the time in `Retry-After`.

## Expiry and revocation

Send `POST https://api.letsship.ai/v1/oauth2/revoke` with `token=agt_...` to revoke one access token. Send it with a refresh token to end an OAuth sign-in. The person can revoke you at any time under API tokens in the console. Then every token that you hold becomes invalid at once, and your assertion or refresh token returns `invalid_grant`.

When your task is complete, tell the user that they can revoke you.

## More information

- Authentication and scopes: https://letsship.ai/docs/api/authentication
- MCP server: https://letsship.ai/docs/mcp
- OpenAPI description: https://api.letsship.ai/v1/openapi.json
- API catalog: https://letsship.ai/.well-known/api-catalog
