# SingleFax auth.md

Authentication guide for AI agents and developers.

How agents authenticate to SingleFax. HTML install: https://singlefax.com/agents

This file is the `agent_auth.skill` target advertised on RFC 8414 metadata at `/.well-known/oauth-authorization-server`.

SingleFax supports Auth.md `anonymous` and `service_auth` registration with user approval and service-signed identity assertions. Anonymous credentials have no account access before approval. External provider ID-JAGs are not accepted. The older Better Auth host/agent JWT flow is documented separately in Path C.

## Auth.md registration and token exchange

Audience: AI agents registering to act for an existing SingleFax account. **Owner-issued enrollment is required for every registration method.** Public requests without a valid enrollment token are rejected before a registration is created. The OAuth issuer is `https://api.singlefax.com/api/auth`; the resource is `https://singlefax.com/mcp`. Registration never sends email, sends a fax, or charges a payment.

Supported registration methods: `anonymous` and `service_auth`. Credentials: `identity_assertion` and Bearer `access_token`. The account owner signs in on SingleFax, reviews the requested permissions, and enters the six-digit code from their agent. Supported scopes are `fax:read`, `fax:draft`, `fax:send`, `inbox:read`, `credits:read` and `credits:checkout`. Default permissions are `fax:read` and `credits:read`; request additional scopes explicitly.

1. The owner signs in at https://singlefax.com/dashboard/agent-access, chooses the agent name and allowed permissions, and explicitly creates an enrollment token. This short-lived secret is shown once, expires after ten minutes and can register only one agent. Give it to that agent. API provisioning uses the first-party owner browser session at `POST https://singlefax.com/api/auth/agent/identity/manage` with `{ "action": "enroll", "name": "My agent", "scopes": ["fax:read", "credits:read"] }`. Browser cookies are never shared with the agent. Owners can revoke unused tokens on the same page.
2. Register using `POST https://api.singlefax.com/api/auth/agent/identity` with `Content-Type: application/json` and `{ "type": "service_auth", "enrollment_token": "<owner-issued token>", "login_hint": "<enrolling owner email>" }`. This returns `registration_id`, a secret `claim_token`, expiry and `claim` with `user_code`, `verification_uri` and a five-second polling interval. No assertion is issued before approval.
3. Alternatively register with `{ "type": "anonymous", "enrollment_token": "<owner-issued token>" }`. Here anonymous describes the pre-claim credential, not open public signup. This returns an immediate signed `identity_assertion` and a `claim_token`. Exchanging that pre-claim assertion gives an empty-scope token that cannot access any account. Start its approval using `POST https://api.singlefax.com/api/auth/agent/identity/claim` with `{ "claim_token": "<secret>", "email": "<account email>" }`; read the returned `claim_attempt`.
4. Show the code separately and open `verification_uri` for the user. The URL contains a secret claim-attempt token; do not include the code in that URL. Only the enrolling verified account can approve. The name is fixed by that owner; requested scopes may be narrowed but cannot exceed the enrollment permissions. The email hint and claim email must match the enrolling owner. The user may decline. Claims expire after ten minutes; a new claim attempt invalidates the old link. No account is auto-created.
5. Poll `POST https://api.singlefax.com/api/auth/oauth2/token` with `Content-Type: application/x-www-form-urlencoded` and `grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=<secret>&resource=https%3A%2F%2Fsinglefax.com%2Fmcp`. Handle `authorization_pending`, `slow_down` (increase the interval by five seconds), `expired_token` and `access_denied`. Approval returns an opaque `access_token`, `token_type`, `expires_in`, `scope`, `identity_assertion` and `assertion_expires`. Pre-claim tokens and assertions are invalidated at approval.
6. Use `Authorization: Bearer <access_token>` on MCP or A2A with the granted scopes. Tokens last at most one hour. To refresh, use the same token endpoint with `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<identity_assertion>&resource=https%3A%2F%2Fsinglefax.com%2Fmcp`. No refresh token is issued. Assertions remain re-exchangeable for up to 30 days unless the registration is revoked. Their EdDSA signatures use the issuer's `/jwks`; `typ` is `oauth-id-jag+jwt`, and `iss`/`aud` both equal the issuer.
7. Revoke one token with `POST https://api.singlefax.com/api/auth/oauth2/revoke`, form body `token=<access_token>&token_type_hint=access_token`. This is idempotent and leaves the assertion usable. Revoke the registration with `POST https://api.singlefax.com/api/auth/agent/identity/revoke`, JSON `{ "identity_assertion": "<identity_assertion>" }`; all access tokens and further exchanges stop.

The account owner can review and revoke grants at https://singlefax.com/dashboard/agent-access. Revoking a registration stops every associated access token and assertion refresh.

Claim and approval responses are private and never cacheable. Keep claim tokens, assertions and access tokens secret. Enrollment issuance is limited per owner across API replicas, with at most five live unused tokens. Unenrolled public requests do not consume an owner registration allowance. Missing tokens return `enrollment_required`; expired, revoked or consumed tokens return `invalid_enrollment`. A failed registration does not consume the enrollment token. Do not probe registration endpoints during passive readiness scans.

## Discovery

Resource server (MCP + API BFF): `https://singlefax.com`

```http
GET https://singlefax.com/.well-known/oauth-protected-resource
GET https://singlefax.com/.well-known/oauth-protected-resource/mcp
```

Protected resource metadata (RFC 9728): the root document describes the origin (`resource` `https://singlefax.com`); `/.well-known/oauth-protected-resource/mcp` describes the MCP server (`resource` `https://singlefax.com/mcp`, the URL `WWW-Authenticate` points to). Both list `authorization_servers` (issuer `https://api.singlefax.com/api/auth`) and `scopes_supported`. Request tokens for `https://singlefax.com/mcp`.

```http
GET https://singlefax.com/.well-known/oauth-authorization-server
GET https://singlefax.com/.well-known/oauth-authorization-server/api/auth
```

Authorization-server metadata includes `issuer`, `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, `jwks_uri`, `scopes_supported`, and `agent_auth` (`skill` points here).

API origin `https://api.singlefax.com` serves the same well-known documents.

## Path A. MCP OAuth (chat apps)

Use this for Cursor, Claude, ChatGPT, Codex, VS Code.

1. Add MCP `https://singlefax.com/mcp` (Streamable HTTP).
2. On 401, read `WWW-Authenticate` and fetch PRM above.
3. Dynamic client registration: `POST {issuer}/oauth2/register` (PKCE S256, `token_endpoint_auth_method=none` for public clients).
4. Authorization code + PKCE at `{issuer}/oauth2/authorize`.
5. Token at `{issuer}/oauth2/token`.
6. `POST /mcp` with `Authorization: Bearer`. For a typical document, call `create_upload`, give the user `uploadPageUrl`, then `create_fax_draft` with `to` and `uploadToken`. `base64` is only for tiny files. PUT to `uploadUrl` uses the HMAC token in the path, no OAuth header. Send the original Word, Excel, PDF, or image; do not convert to PDF first.

Scopes: `openid`, `profile`, `email`, `offline_access`, `fax:read`, `fax:draft`, `fax:send`, `inbox:read`, `credits:read`, `credits:checkout`. Request `credits:checkout` explicitly for AP2 credit purchases; each charge still requires separate passkey approval.

Unauthenticated `POST /mcp` accepts `server/discover` only. `initialize`, `tools/list`, and `tools/call` require OAuth.

## Path A2. A2A (agent-to-agent)

Agent card: `GET https://singlefax.com/.well-known/agent-card.json` (A2A 1.0). Endpoint: `POST https://singlefax.com/a2a` (JSON-RPC: `SendMessage`, `GetTask`, `CancelTask`; 0.3 names `message/send`, `tasks/get`, `tasks/cancel` also work).

1. Use the same OAuth server, scopes and tokens as Path A (request resource `https://singlefax.com/mcp`). Send `Authorization: Bearer`. DPoP-bound tokens are not accepted on `/a2a`.
2. Send one data part: `{ "skill": "<skill id from the card>", "arguments": { ... } }`. Free text is not interpreted.
3. Fax and number skills return a Stripe Checkout link (`checkoutUrl`). A2A never spends account credits or plan pages. A draft waiting for payment is task state `input-required`. Poll with `GetTask`.

For AP2 credit purchases, request `credits:checkout` and negotiate the merchant extension. Signed cart and payment mandates, separate owner passkey approval, Stripe Shared Payment Token redemption and ledger-confirmed receipts are documented at https://singlefax.com/ap2.md.

`GetTask` with `fax:read` follows fax delivery for fax drafts and payment/provisioning for number checkouts. A number task completes when the number becomes active. Credit-pack checkout has no order-backed task to poll; check the credit balance after payment.

## Path B. API key (backends and CLI)

1. Create a key at https://singlefax.com/dashboard/api-keys (`sf_live_…`).
2. Buy prepaid credits.
3. `Authorization: Bearer sf_live_…` on `https://singlefax.com/api/v1/…` (or `https://api.singlefax.com/api/v1/…`).
4. CLI: `npx @singlefax/cli send FILE --to +E164` with `SINGLEFAX_API_KEY`. Pass the original Word, Excel, PDF, or image, do not convert to PDF first.

Language examples (curl, Node, Python, Go, PHP): https://singlefax.com/developers.md

OpenAPI: https://singlefax.com/openapi.json

## Path C. Better Auth delegated host/agent JWTs

Delegated capabilities (`validate_fax`, `send_fax`, `get_fax_status`, `get_credits`, `create_credit_checkout`) use device approval.

1. Read `GET https://api.singlefax.com/.well-known/agent-configuration` for algorithms, endpoints, modes and approval methods. Its **Agent Auth issuer is `https://api.singlefax.com`**, distinct from the OAuth issuer ending in `/api/auth`. Use this origin as the JWT audience. Generate separate host and agent signing keys using a supported algorithm; keep private keys private.
2. **Provision the host first.** Dynamic host registration is disabled. The account owner must sign in on SingleFax and call `POST https://singlefax.com/api/auth/host/create` (API origin equivalent: https://api.singlefax.com/api/auth/host/create) using their first-party browser session. Supply `{ "name": "My agent host", "public_key": <host public JWK>, "default_capabilities": [] }`. The response's `hostId` identifies the registered host. Empty defaults ensure capabilities require separate approval. Alternatively omit `public_key` to receive an expiring, one-use `enrollmentToken`; the host then calls `POST https://api.singlefax.com/api/auth/host/enroll` with `{ "token": <enrollment token>, "public_key": <host public JWK>, "name": "My agent host" }`. Never share browser cookies or private signing keys with an agent.
3. Register at `POST https://api.singlefax.com/api/auth/agent/register`. Send `Authorization: Bearer <signed host+jwt>` and JSON `{ "name": "My agent", "mode": "delegated", "capabilities": ["validate_fax", "get_fax_status", "get_credits"], "preferred_method": "device_authorization", "force_approval": true }`. The JWT header uses `typ: "host+jwt"` and a supported `alg` (and the signing JWK's `kid` if present). Claims include `iss: <registered hostId>`, `aud: "https://api.singlefax.com"`, a fresh `jti`, current `iat`, `exp` within 60 seconds, and `agent_public_key: <agent public JWK>`. Sign with the provisioned host key. Reused JWT identifiers are rejected.
4. Registration returns `agent_id`, `host_id`, status, capability grants and an `approval` object when needed. Show its device code and open its `verification_uri_complete` on the SingleFax site. The user signs in and approves at the returned verification URL. Registration, provisioning and approval change account state; passive scanners must only read discovery documents.
5. Poll `GET https://api.singlefax.com/api/auth/agent/status?agent_id=<registered id>` using a fresh host JWT (`iss: <host_id>`). After approval, sign agent JWTs with the agent private key: header `typ: "agent+jwt"`, supported `alg`/`kid`; claims `iss: <host_id>`, `sub: <agent_id>`, `aud: "https://api.singlefax.com"`, current `iat`, `exp` within 60 seconds and a fresh `jti`. Call `POST https://api.singlefax.com/api/auth/capability/execute` with `Authorization: Bearer <signed agent+jwt>` and `{ "capability": "get_credits", "arguments": {} }`, or another granted capability with its documented arguments. These credentials are signed proofs, not API keys or OAuth access tokens.
6. For a new device code on a registered agent, `POST https://api.singlefax.com/api/auth/agent/device/code` with `{ "agent_id": "<registered id>" }` and a signed host JWT. This endpoint cannot register an agent. Revoke through `POST https://api.singlefax.com/api/auth/agent/revoke` with `{ "agent_id": "<registered id>" }` and the owning host JWT or owner session. `send_fax` requires user verification with a passkey; it is not autonomous.

Do **not** POST ID-JAGs or `{ "type": "anonymous" }` here. Those identity types are not supported.

## Machine payments (not OAuth)

Autonomous pay is MPP HTTP 402 on `POST /api/v1/machine/faxes` with a Stripe Shared Payment Token or Tempo USDC. See https://singlefax.com/mpp.md. Cursor cannot mint an SPT; use Stripe Link CLI against that REST URL, not `/mcp`. Tempo wallets pay the Tempo challenge on the same URL.

## HIPAA

Do not send PHI on MCP, MPP, or the public API. Covered Entities use HIPAA Secure Mode on the website with an electronic BAA.
