SingleFax authentication docs

API keys, OAuth for MCP, and delegated Agent Auth.

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

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.

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.