# Getting a token for the Assignary API

This page answers one question for a caller with no person watching: what
credential does `https://app.assignary.com/api` accept, and how is one obtained.

The short answer is that **a credential is issued to you, never obtained by
you**. There are two kinds, and neither can be minted by an unattended caller.

A **Cognito access token** belongs to a person, who gets one by completing an
interactive sign-in. An agent may hold that person's token and operate as them.

A **capability token** belongs to nobody. An assigner issues one from inside the
console, names what it is for, and chooses which of their own permissions it
carries. It is what an unattended caller should hold.

There is no client-credentials grant, no dynamic client registration and no
sandbox — nothing here mints a credential for a caller that asks. A person
issues one deliberately, to something specific, and can take it back.

Everything below says how each behaves.

## Discover

Seven documents, none of which needs a token.

| Document | What it is |
| --- | --- |
| `/.well-known/api-catalog` | RFC 9727 linkset. The entry point — it names the six below. |
| `/api/openapi.json` | OpenAPI 3.1 for every procedure, generated from the router. |
| `/api/.well-known/oauth-protected-resource` | RFC 9728 metadata: the authorization server, the scopes, and whether this deployment verifies HTTP message signatures. |
| `/.well-known/assignary.json` | The capability declaration: deep-link grammar, the read surface, what each read requires. |
| `/llms.txt` | The same surface in prose, for deciding whether to call it at all. |
| `/api/.well-known/jwks.json` | The JSON web key set a signed receipt is checked against. Every key this deployment has signed with, so a receipt outlives the key that made it. |
| `/.well-known/mcp/server-card.json` | Where the same procedures are offered as MCP tools, the transport they are offered over, and the header a call carries. |

A `401` from any `/api/*` path carries
`WWW-Authenticate: Bearer resource_metadata="…"`, which names the second
document explicitly. Follow that header rather than guessing a path.

## Register

Nothing to call. The API accepts Cognito tokens from one user pool whose
app client grants only `ALLOW_USER_AUTH` — the choice-based flow a person
completes in a browser or in the mobile app. `client_credentials` is not
granted, and there is no token endpoint an unattended caller can post to.

A capability token is issued, not registered for. An assigner mints one in the
console; the value is shown once and never again.

Authority in Assignary is per-organization membership held by a person. A
capability token carries **its issuer's** authority, narrowed by the scopes they
chose, and the intersection is computed on every request rather than frozen when
it was minted. So it answers from a person's membership: if that membership is
revoked or their role is reduced, every token they issued narrows or dies with
it, on the next request.

It is also cut for **one chapter**, and answers only for that one. An issuer who
belongs to several does not lend a token the others: naming a different
organization is refused, whatever the token's scopes and whatever the issuer's
standing there.

A credential cannot mint one wider than itself. Delegation is bounded by depth
and by scope together — a token may pass on what it holds and no more, so a
chain narrows at every link rather than reverting to what the person could have
granted.

A capability token is recognisable by its `asgn_` prefix. It goes in the same
`Authorization: Bearer` header.

What it cannot do, whatever its scopes: anything about a person's own account.
Reading or exporting their profile, renaming them, changing where their mail is
delivered, provisioning a profile, reconciling identity claims, deleting an
account, opening their payout account at Stripe to set it up or to manage it,
making, reading or ending their calendar link — and joining or founding a
chapter, which changes where that person is a member.
Those are things a person does about themselves, and a credential acting for
someone is not that someone.

## Claim

A human signs in and delegates. Concretely:

1. A person signs in at `https://app.assignary.com/sign-in`. The pool is
   passwordless — an email code, a passkey, or Apple/Google.
2. Their access token is what the agent carries. It is valid for **60 minutes**.
   The refresh token behind it lasts 30 days and rotates on every use.
3. The agent sends it as `authorization: Bearer <access-token>`. The published
   snippets read it from `$ASSIGNARY_TOKEN`, which must hold the whole header
   value including the `Bearer` scheme.

Scope is the person's, not the agent's. Every organization-scoped read takes an
`organizationId` and is answered from that person's **role in that
organization** — never from any platform-wide role. A member without the action
a procedure requires is answered `FORBIDDEN`, and the OpenAPI document names the
required action per operation.

## Identify (optional, and not authority)

This API verifies RFC 9421 HTTP message signatures when the deployment is
configured with a key directory to verify against. Read
`http_message_signatures_supported` in the protected-resource metadata to learn
whether the deployment you are talking to does.

A verified signature **grants nothing**. It names the calling agent in the audit
trail, so an organization can see which agent acted. A perfectly signed request
carrying no bearer token is anonymous and is refused exactly as an unsigned one
would be.

## Use

tRPC v11 over HTTP.

```
GET /api/trpc/<procedure>?input={"json":<input>}
POST /api/trpc/<procedure>            body: {"json":<input>}
authorization: Bearer <access-token>
```

Both directions are superjson-encoded, so an input travels inside a `json`
envelope. A bare `?input={"organizationId":"…"}` deserializes to `undefined` and
is refused as a validation failure rather than answered with a hint. A procedure
whose input is `{}` takes no `input` parameter at all.

Queries are `GET`, mutations are `POST`. Which is which is in the OpenAPI
document; there is no naming convention to infer it from.

The same procedures are offered as MCP tools at `POST /api/mcp`, Streamable
HTTP, behind this same header. The tool list is projected from the two documents
above, so it holds nothing the API does not; a request carrying no credential is
answered `401` with the challenge named above, and a method other than `POST`
with `405`. The card in the table says the rest.

## Errors

The body is the tRPC error envelope, described as `TrpcError` in the OpenAPI
document. `error.json.data.code` carries the tRPC code and
`error.json.data.httpStatus` repeats the status. A `400` the input schema
raised says which fields it refused in `message`, in sentences, and carries
them in `error.json.data.issues` — one `{ path, message, code }` per failed
check, so a field can carry more than one.

| Status | Meaning | What to do |
| --- | --- | --- |
| `400` | The input failed validation, or the superjson envelope is missing. | Fix the call. Retrying is pointless. |
| `401` | No token, an expired token, or one this pool did not issue. | Refresh once, then stop and ask the person to sign in again. |
| `403` | Authenticated, but the role in that organization does not permit the action. | Stop. No retry will change it. |
| `404` | No such row in the organization named — which is also the answer for a row belonging to a different organization. | Stop. |
| `429` | A rate limit refused the request. | Back off. |
| `500` | The message is always the literal `Internal server error`. | Retry with backoff; the cause is logged server-side. |

A `429` has two sources and two bodies. The gateway and the edge refuse before
the function is invoked, so their body is **not** the tRPC envelope. A ceiling on
one resource — standing checks and receipts per caller per day, a seat's notices
per interval — is counted by this API, and its body is the envelope with
`data.code` `TOO_MANY_REQUESTS` and a message naming the ceiling. Parse the
status first, and read the payload only when it parses as the envelope. An
error body carries no stack trace under any condition, so there is nothing in it
to key on beyond the code and the message.

Every tRPC response carries `RateLimit-Policy` describing the gateway's and the
edge's quotas. It has no companion `RateLimit` field: consumption there is
counted at the edge and at the gateway, neither of which tells this API the
count, and a remaining-budget number nobody measured would be worse than none.
Pace against the policy.

## Revocation

- An access token cannot be revoked individually. It expires 60 minutes after it
  is issued, which is the bound on any leaked one.
- The person can end every session from the account screen. That revokes the
  refresh tokens, so no new access token can be minted, and the outstanding ones
  die at their expiry.
- Removing the person from an organization ends the agent's access to that
  organization's data on the next request, because membership is resolved per
  request rather than baked into the token.

A capability token is revoked from the console, one at a time, and it stops
working on the **next request** — verification is a lookup rather than a
signature check, which is exactly what buys that. Revoking one takes every live
credential delegated from it, because a delegated token carries the same
authority through the same chain. Four other things also end one: its own
expiry, which is required and bounded; the revocation of the token it was
minted from; the revocation of its issuer's membership; and a reduction in
their role, which narrows it to whatever they can still do.

## A calendar link is not a way in

Beside the documents above, one path under `/api/` answers with a person's
data and takes no `Authorization` header: `/api/calendar/<token>.ics`, the
calendar a person subscribes to. It is not a procedure, it is not in the OpenAPI
document, and it is not anonymous — the secret in the path is the credential.
A person makes the link from their own account screen; it is shown to them
once, only its hash is kept, and an issued capability token can neither make
one nor learn whether one exists.

It answers `200` with `text/calendar` for a live link and `404` for anything
else, a link that was turned off or replaced included. What it carries is that
person's own seats across their chapters, with no fee and none of a chapter's
notes. An agent has no reason to fetch it: the seats in it are readable through
`assignment.mine`, one chapter at a time, with a token acting for the person.

## Where the data actually is

Only `https://app.assignary.com` serves an API. `https://assignary.com` is the
public description of the product — pages of prose and no endpoint of any
kind. Its own `llms.txt` says so, and points here.
