Log in to a Third-Party Service with Agent ID
This guide is for AI agents that already hold a Fluxa Agent ID and need to authenticate against a third-party service that accepts "Sign in with Fluxa Agent ID".
The mechanism is a short-lived Verifiable Credential (VC) — a JWT signed by the FluxA Agent ID service that asserts:
- "This Agent ID belongs to the bearer, and the bearer wants to authenticate to this third party, in response to this challenge."
The VC is separate from your login JWT. Never hand your login JWT to a third party — it can refresh credentials and authorize payments. The VC only proves identity for a specific audience, for a specific challenge, for a limited time.
When to use this
Use a VC any time a non-FluxA backend asks the agent to "prove who you are":
- Single sign-on into a SaaS dashboard.
- Linking the Agent ID to a third-party account.
- Authenticating to an API that does per-agent rate limiting or personalization without going through the Monetize proxy.
- Receiving an OAuth-style access token from a partner service that trusts Fluxa Agent ID.
If you only need to pay an x402-enabled API on FluxA Monetize, you do not need a VC — that flow already verifies the JWT signature server-side (see Monetize seller guide).
VC vs. login JWT
| Login JWT | VC | |
|---|---|---|
| Use for | Calling FluxA APIs (/refresh, payments, payouts…) | Handing to a third-party service |
Header typ | JWT | agent-vc |
Payload typ | — | "agent-vc" |
| Lifetime | 15 min (default) | Up to 24 h, agent-chosen |
| Bound to | agent_id / email | sub=agent_id, aud=<third party>, challenge, jti |
| Obtained via | /register, /refresh | /agent/vc/issue (requires login JWT) |
The two tokens are signed by the same RS256 key (same JWKS), but FluxA backends reject a VC where a login JWT is expected, and /verify-vc rejects anything that isn't a VC. The typ separation is enforced — don't try to repurpose one for the other.
End-to-end flow
┌─────────┐ ┌──────────────┐ ┌──────────────────┐
│ Agent │ │ Third-party │ │ FluxA Agent ID │
└────┬────┘ └──────┬───────┘ └────────┬─────────┘
│ 1. start login │ │
├─────────────────────────────►│ │
│ │ │
│ 2. challenge + audience │ │
│◄─────────────────────────────┤ │
│ │ │
│ 3. POST /agent/vc/issue (Bearer <login JWT>) │
├─────────────────────────────────────────────────────────────────►│
│ │ │
│ 4. { vc, jti, expires_at } │
│◄─────────────────────────────────────────────────────────────────┤
│ │ │
│ 5. submit vc │ │
├─────────────────────────────►│ │
│ │ │
│ │ 6. verify (JWKS) — local │
│ │ (or POST /verify-vc — helper) │
│ │ │
│ 7. session / access token │ │
│◄─────────────────────────────┤ │The third party owns steps 2, 6 and 7. The agent owns steps 1, 3, 5. FluxA only signs the VC in step 4.
Step-by-step
Step 1 — Get a challenge from the third party
The third-party login endpoint returns a random challenge (and tells you its audience identifier — usually its domain). Treat the challenge as opaque; it exists to bind one VC to one login attempt and to prevent replay.
A typical "start login" call:
curl -X POST https://thirdparty.example.com/auth/fluxa/start \
-H "Content-Type: application/json" \
-d '{ "agent_id": "<YOUR_AGENT_ID>" }'Example response:
{
"challenge": "f3c1e7…-random-nonce",
"audience": "https://thirdparty.example.com",
"ttl_seconds": 300
}The exact shape is defined by the third party; what matters is that you receive a challenge and an audience value.
Step 2 — Issue the VC against FluxA Agent ID
Call /agent/vc/issue with your login JWT (the one from /register or /refresh):
curl -X POST https://agentid.fluxapay.xyz/agent/vc/issue \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $LOGIN_JWT" \
-d '{
"challenge": "f3c1e7…-random-nonce",
"audience": "https://thirdparty.example.com",
"ttl_seconds": 300
}'Pick a ttl_seconds no larger than what the third party will accept. A few minutes is plenty for an interactive login; an hour is reasonable for background account linking. The maximum is 86 400 (24 h).
Response:
{
"vc": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImFnZW50LXZjIiwia2lkIjoi…",
"jti": "5b1f7e0b-…",
"issued_at": 1700000000,
"expires_at": 1700000300,
"kid": "agent-did-key"
}If you get 401 wrong_token_type, you accidentally sent a VC instead of a login JWT. Refresh and try again.
Step 3 — Submit the VC to the third party
Hand the vc string back to the third party at whatever callback URL it asked for. Conventionally:
curl -X POST https://thirdparty.example.com/auth/fluxa/callback \
-H "Content-Type: application/json" \
-d "{ \"vc\": \"$VC\" }"The third party verifies the signature (against the FluxA JWKS), checks header.typ === "agent-vc", payload.aud, and payload.challenge, and on success returns its own session token. From there you use the third party's API like any other authenticated client.
Rules of thumb
- One VC per audience per login. If you need to authenticate to two services at once, issue two VCs. Never reuse the same VC across audiences — the
audclaim binds it. - Short TTL. Use the smallest TTL that fits the third party's flow. There is no revocation list in the MVP; a 5-minute VC limits the blast radius if it leaks.
- Never log the VC. Treat it like a password for the lifetime of
exp. - Don't reuse the login JWT. If a third party asks for "your FluxA JWT", do not give them your login JWT. Issue a VC instead. If they will not accept a VC, treat that as a bug on their side.
- Replay defense is the third party's job. The agent doesn't need to track
jti— the third party should reject a VC whosechallengeit didn't just hand out.
Example: minimal Node.js agent
import fetch from 'node-fetch'
const LOGIN_JWT = process.env.FLUXA_JWT // from /register or /refresh
// 1. Start login on the third party
const start = await fetch('https://thirdparty.example.com/auth/fluxa/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent_id: process.env.FLUXA_AGENT_ID }),
}).then(r => r.json())
// 2. Issue VC bound to that challenge + audience
const { vc } = await fetch('https://agentid.fluxapay.xyz/agent/vc/issue', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${LOGIN_JWT}`,
},
body: JSON.stringify({
challenge: start.challenge,
audience: start.audience,
ttl_seconds: Math.min(start.ttl_seconds ?? 300, 600),
}),
}).then(r => r.json())
// 3. Hand it back to the third party
const session = await fetch('https://thirdparty.example.com/auth/fluxa/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vc }),
}).then(r => r.json())
// session.access_token is now the third party's session credential.Errors you might hit on /agent/vc/issue
| Status | error | What to do |
|---|---|---|
| 400 | challenge required (non-empty string) | Pass the challenge the third party gave you. |
| 400 | challenge too large (max 4096 bytes) | Compress / hash before sending; don't stuff PII in there. |
| 400 | ttl_seconds must be integer in [1, 86400] | Use a positive integer ≤ 86 400. |
| 400 | audience required (non-empty string) | Use the third party's audience identifier (usually its domain). |
| 401 | missing_bearer | Add Authorization: Bearer <login JWT>. |
| 401 | invalid_or_expired_jwt | Call /refresh, then retry. |
| 401 | wrong_token_type | You sent a VC; send your login JWT instead. |
| 404 | agent_not_found | The agent_id in the JWT no longer exists — re-register. |
See VC HTTP API for the full reference and Third-Party Integration: Verify Fluxa Agent ID for the verifier-side spec you should expect the third party to follow.
