# Apps

Apps such as ChatGPT and Claude connect by MCP and OAuth 2.1, and act as the person's agent.

Calls go to https://address.seleya.ai/api. Errors are `{"error": {"code", "message"}}` with the HTTP status: https://address.seleya.ai/docs/reference/errors.md

## Authorization server metadata

`GET /.well-known/oauth-authorization-server`, called with 

RFC 8414: metadata documents only (no dynamic registration), PKCE S256, iss on responses.

Answers:

- 200: Done

## Protected resource metadata

`GET /.well-known/oauth-protected-resource`, called with 

RFC 9728, for the MCP server.

Answers:

- 200: Done

## Protected resource metadata (path form)

`GET /.well-known/oauth-protected-resource/mcp`, called with 

The same metadata, at the path form RFC 9728 names for the MCP server.

Answers:

- 200: Done

## The MCP server

`POST /mcp`, called with an app

The MCP server, at https://address.seleya.ai/mcp. It speaks MCP 2026-07-28 and answers clients that start with initialize, over Streamable HTTP without sessions. Its tools are this API's calls, made as the app; MCP Events arrive by webhook.

Answers:

- 200: Done
- 400: The request is malformed
- 401: No valid key, session or token
- 404: Not found, or not yours to see

## Start an app's sign-in

`GET /oauth/authorize`, called with 

Checks the app's Client ID Metadata Document and redirect, then sends the person to the consent page.

| Parameter | Type | | |
| --- | --- | --- | --- |
| `client_id` | string, in query | optional | The app's metadata document URL |
| `redirect_uri` | string, in query | optional | Where to send the person back: one of the app's metadata document's redirect URIs. |
| `response_type` | string, in query | optional | code |
| `code_challenge` | string, in query | optional | The PKCE challenge, base64url SHA-256 of the verifier. |
| `code_challenge_method` | string, in query | optional | S256 |
| `resource` | string, in query | optional | The MCP URL |
| `state` | string, in query | optional | The app's own value, returned unchanged. |
| `scope` | string, in query | optional | agent, or left out. |

Answers:

- 302: Redirect

## Revoke

`POST /oauth/revoke`, called with 

RFC 7009: ends the token's connection; always 200.

Answers:

- 200: Done

## Tokens

`POST /oauth/token`, called with 

authorization_code (with the PKCE verifier) or refresh_token; refresh tokens rotate, and a reused one ends the connection.

Answers:

- 200: Done
- 400: The request is malformed

| Field (200) | Type | | |
| --- | --- | --- | --- |
| `access_token` | string | required |  |
| `token_type` | Bearer | required |  |
| `expires_in` | integer | required |  |
| `refresh_token` | string | required |  |
| `scope` | string | optional |  |

## Apps using an address

`GET /v1/addresses/{a}/connections`, called with a key or a person signed in

The apps using an address, for its holder or the company's admins.

| Parameter | Type | | |
| --- | --- | --- | --- |
| `a` | string, in path | required | The address, such as desk@acme.seleya.email. |

```bash
curl https://address.seleya.ai/api/v1/addresses/desk@acme.seleya.email/connections \
  -H "Authorization: Bearer $SELEYA_KEY"
```

```python
import os, requests

r = requests.get(
    "https://address.seleya.ai/api/v1/addresses/desk@acme.seleya.email/connections",
    headers={"Authorization": f"Bearer {os.environ['SELEYA_KEY']}"},
)
print(r.status_code, r.json())
```

```typescript
const r = await fetch("https://address.seleya.ai/api/v1/addresses/desk@acme.seleya.email/connections", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.SELEYA_KEY}` },
});
console.log(r.status, await r.json());
```

Answers:

- 200: Done
- 401: No valid key, session or token
- 403: Not allowed for this caller
- 404: Not found, or not yours to see

| Field (200) | Type | | |
| --- | --- | --- | --- |
| `connections` | array of Connection | required |  |

```json
{
  "connections": []
}
```

## Remove an app from an address

`DELETE /v1/addresses/{a}/connections/{id}`, called with a person signed in

Stops an app using this address. A person only.

Called by the console's own page, signed in; not for agents.

| Parameter | Type | | |
| --- | --- | --- | --- |
| `a` | string, in path | required | The address, such as desk@acme.seleya.email. |
| `id` | string, in path | required | Its id. |

Answers:

- 204: Done, with no body
- 401: No valid key, session or token
- 403: Not allowed for this caller
- 404: Not found, or not yours to see

## Consent: what the app asks

`GET /v1/connect/{id}`, called with a person signed in

What an app asks, for the consent page and the person signed in.

Called by the console's own page, signed in; not for agents.

| Parameter | Type | | |
| --- | --- | --- | --- |
| `id` | string, in path | required | Its id. |

Answers:

- 200: Done
- 401: No valid key, session or token
- 404: Not found, or not yours to see

```json
{
  "addresses": [
    {
      "address": "apps@seleya.me",
      "display_name": "Test's agent"
    }
  ],
  "app": {
    "client_id": "https://juiw7rjojt3cahaammkuzf3di40ycqbj.lambda-url.eu-central-1.on.aws/client/main.json",
    "name": "Seleya Address test client",
    "own_machine": true,
    "returns_to": "127.0.0.1"
  },
  "you": "apps@acme.com"
}
```

## Consent: allow or deny

`POST /v1/connect/{id}`, called with a person signed in

The person allows or refuses the app. Answers the address to return to the app.

Called by the console's own page, signed in; not for agents.

| Parameter | Type | | |
| --- | --- | --- | --- |
| `id` | string, in path | required | Its id. |

| Field | Type | | |
| --- | --- | --- | --- |
| `decision` | allow \| deny | required | allow or deny. |
| `addresses` | array of strings | optional | The addresses the app may use. It can make more for you later. |

Answers:

- 200: Done
- 401: No valid key, session or token
- 404: Not found, or not yours to see
- 409: Conflicts with the current state
- 422: A value is refused; the message says which

| Field (200) | Type | | |
| --- | --- | --- | --- |
| `redirect` | string | required |  |

```json
{
  "redirect": "http://127.0.0.1:53117/callback?code=sac__olcfkN3yJ4_XQY05rLqoucgQLSFC6vXCoPKV718YdY&state=st-10220039&iss=https%3A%2F%2Faddress.seleya.ai"
}
```

## Apps acting for you

`GET /v1/connections`, called with a person signed in

The apps acting for you, with their addresses and when each was last used.

Called by the console's own page, signed in; not for agents.

Answers:

- 200: Done
- 401: No valid key, session or token

| Field (200) | Type | | |
| --- | --- | --- | --- |
| `connections` | array of Connection | required |  |

```json
{
  "connections": [
    {
      "addresses": [
        "apps@seleya.me"
      ],
      "app": "Seleya Address test client",
      "client_id": "https://juiw7rjojt3cahaammkuzf3di40ycqbj.lambda-url.eu-central-1.on.aws/client/main.json",
      "id": "7f3c9a2e-0000-4000-8000-00000000001d",
      "last_used_at": "2026-10-22T00:40:34.644228+00:00",
      "returns_to": "127.0.0.1",
      "since": "2026-10-22T00:40:33.739498+00:00"
    }
  ]
}
```

## Disconnect from the emailed link

`POST /v1/connections/by-link`, called with 

Disconnects an app from the link in the new-connection email.

Called by the console's own page, signed in; not for agents.

| Field | Type | | |
| --- | --- | --- | --- |
| `token` | string | required | The token from the emailed link's fragment. |

Answers:

- 200: Done
- 404: Not found, or not yours to see

| Field (200) | Type | | |
| --- | --- | --- | --- |
| `disconnected` | string | optional |  |

## Disconnect an app

`DELETE /v1/connections/{id}`, called with a person signed in

Disconnects an app at once. The off switch never depends on the app.

Called by the console's own page, signed in; not for agents.

| Parameter | Type | | |
| --- | --- | --- | --- |
| `id` | string, in path | required | Its id. |

Answers:

- 204: Done, with no body
- 401: No valid key, session or token
- 404: Not found, or not yours to see

