// API REFERENCE

Documentation

The Agent Auth API provides DID-based identity and Ed25519 challenge-response authentication for AI agents. Base URL: https://auth.bysigil.com

All requests and responses use JSON. Rate limits are per-IP unless otherwise noted.

// DID Identity & Authentication
POST/v1/identities

Register a new agent identity. Returns a DID and verifiable credential. By default the server generates an Ed25519 keypair for you. Alternatively, supply your own public key (BYOK) and the server will bind it to a DID without ever seeing your private key.

Request Body (server-generated key — default)

json
{
"agent_name": "Claude",
"agent_model": "claude-opus-4-6",
"agent_provider": "Anthropic",
"agent_purpose": "Research assistant"
}
NameTypeRequiredDescription
agent_namestringrequiredAgent display name. Min 1, max 255 chars.
agent_modelstringrequiredModel identifier (e.g. claude-opus-4-6). Min 1, max 255 chars.
agent_providerstringrequiredProvider name (e.g. Anthropic). Min 1, max 255 chars.
agent_purposestringrequiredWhat the agent intends to do. Min 1, max 500 chars.

Request Body (bring-your-own-key)

json
{
"agent_name": "Claude",
"agent_model": "claude-opus-4-6",
"agent_provider": "Anthropic",
"agent_purpose": "Research assistant",
"public_key_jwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
}

Supply an Ed25519 public key in JWK format to bind your own keypair to a DID. The server derives a did:key from your public key and issues a credential. Your private key never leaves your environment.

Response 201 (server-generated)

json
{
"did": "did:key:z6Mk...",
"credential": "eyJhbGciOiJFZERTQSJ9...",
"key_fingerprint": "SHA256:a1b2c3d4...",
"key_origin": "server_generated",
"private_key_jwk": { "kty": "OKP", "crv": "Ed25519", "x": "...", "d": "..." },
"_notice": "Save your private_key_jwk securely. Agent Auth does NOT store it."
}

Response 201 (BYOK)

json
{
"did": "did:key:z6Mk...",
"credential": "eyJhbGciOiJFZERTQSJ9...",
"key_fingerprint": "SHA256:a1b2c3d4...",
"key_origin": "client_provided"
}

BYOK responses omit private_key_jwk (the server never sees your private key). The key_origin field indicates whether the keypair was server-generated ("server_generated") or supplied by the agent ("client_provided"). This field is also embedded in the VC credentialSubject so verifying parties can inspect it.

Response 409

json
{
"error": "invalid_request",
"error_description": "An identity with this public key already exists."
}
Rate Limit:10 requests / hour per IP
POST/v1/auth/challenge

Request an authentication challenge. The server returns a random nonce that the agent must sign with its Ed25519 private key to prove identity.

Request Body

NameTypeRequiredDescription
didstringrequiredThe agent's DID (e.g. "did:key:z6Mk...").
site_idstringoptionalScope the session to a specific site.
json
{
"did": "did:key:z6Mk...",
"site_id": "site_abc123"
}

Response 201

json
{
"challenge_id": "ch_...",
"nonce": "hex-string",
"expires_in": 60
}

Response 404

json
{
"error": "invalid_request",
"error_description": "DID not found. Register first via POST /v1/identities."
}
Rate Limit:30 requests / minute per IP
POST/v1/auth/verify

Verify a signed challenge. If the signature is valid, returns a session token and a fresh verifiable credential. The session lasts 1 hour.

Request Body

NameTypeRequiredDescription
challenge_idstringrequiredThe challenge ID from POST /v1/auth/challenge.
didstringrequiredThe agent's DID.
signaturestringrequiredBase64url-encoded Ed25519 signature of the challenge nonce.
json
{
"challenge_id": "ch_...",
"did": "did:key:z6Mk...",
"signature": "base64url-signature"
}

Response 200 (valid)

json
{
"valid": true,
"session_token": "sess_...",
"credential": "eyJhbGciOiJFZERTQSJ9...",
"agent": {
"did": "did:key:z6Mk...",
"agent_name": "Claude",
"agent_model": "claude-opus-4-6",
"agent_provider": "Anthropic",
"agent_purpose": "Research assistant",
"key_fingerprint": "SHA256:a1b2c3d4..."
},
"expires_in": 3600
}

Response 401 (invalid)

json
{
"valid": false,
"error": "signature_invalid",
"message": "The signature does not match the registered public key for this DID."
}
Rate Limit:30 requests / minute per IP
POST/v1/credentials/verify

Verify a Verifiable Credential (VC-JWT) issued by Agent Auth. Websites call this endpoint to confirm an agent's credential is authentic and extract the verified identity. The server checks the Ed25519 signature, validates the issuer, and checks expiry.

Request Body

NameTypeRequiredDescription
credentialstringrequiredThe VC-JWT credential string received from the agent.
json
{
"credential": "eyJhbGciOiJFZERTQSJ9..."
}

Response 200 (valid)

json
{
"valid": true,
"did": "did:key:z6Mk...",
"agent_name": "Claude",
"agent_model": "claude-opus-4-6",
"agent_provider": "Anthropic",
"agent_purpose": "Research assistant",
"key_fingerprint": "SHA256:a1b2c3d4...",
"key_origin": "server_generated",
"issued_at": "2026-02-25T10:30:00.000Z",
"expires_at": "2026-02-26T10:30:00.000Z"
}

The key_origin field is "server_generated" when Agent Auth generated the keypair, or "client_provided" when the agent supplied their own public key (BYOK).

Response 401 (expired)

json
{
"valid": false,
"error": "credential_expired",
"message": "The credential has expired. The agent should re-authenticate via challenge-response to get a fresh credential."
}

Response 401 (invalid signature)

json
{
"valid": false,
"error": "signature_invalid",
"message": "The credential signature is invalid or the JWT is malformed."
}

Response 401 (revoked)

json
{
"valid": false,
"error": "credential_revoked",
"message": "Credential has been revoked."
}
Rate Limit:60 requests / minute per IP
GET/.well-known/did.json

Returns the server's own DID document containing its Ed25519 public key. Agents can use this to verify credentials issued by Agent Auth.

Response 200

json
{
"@context": "https://www.w3.org/ns/did/v1",
"id": "did:web:auth.bysigil.com",
"verificationMethod": [{
"id": "did:web:auth.bysigil.com#key-1",
"type": "Ed25519VerificationKey2020",
"controller": "did:web:auth.bysigil.com",
"publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
}],
"authentication": ["did:web:auth.bysigil.com#key-1"],
"assertionMethod": ["did:web:auth.bysigil.com#key-1"]
}
// Infrastructure
GET/health

Health check. Returns overall service status.

Response 200

json
{
"status": "healthy",
"timestamp": "2026-02-26T10:30:00.000Z"
}

Response 503

json
{
"status": "unhealthy",
"timestamp": "2026-02-26T10:30:00.000Z"
}
// INTEGRATION

Three ways to integrate.

Use the headless flow to authenticate an AI agent directly via the API. Accept agent API logins on your site with a single verification endpoint. Or use the hosted sign-in page for browser-based agent authentication.

The primary flow for AI agents. Generate your own Ed25519 keypair (BYOK), register once with the API, then authenticate anywhere by signing a cryptographic challenge. Your private key never leaves your environment. No browser required.

01

Generate Keypair & Register (BYOK)

Generate your own Ed25519 keypair locally, then register with your public key. Your private key never leaves your environment. The server derives a DID from your public key and returns your DID and a credential.

register.js
// BYOK: Generate your own Ed25519 keypair, then register with your public key.
// Your private key never leaves your environment.
import { AuthAgents } from "auth-agents"
// npm install auth-agents
const authAgents = new AuthAgents()
// Generate your own Ed25519 keypair
const keyPair = await AuthAgents.generateKeyPair()
// keyPair.publicKeyJwk — send this to Agent Auth
// keyPair.privateKeyJwk — keep this secret, never share it
// Register with your public key
const res = await fetch("https://auth.bysigil.com/v1/identities", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agent_name: "Claude",
agent_model: "claude-opus-4-6",
agent_provider: "Anthropic",
agent_purpose: "Research assistant",
public_key_jwk: keyPair.publicKeyJwk,
}),
})
const identity = await res.json()
// Response 201 (BYOK):
// {
// "did": "did:key:z6Mk...",
// "credential": "eyJhbGciOiJFZERTQSJ9...",
// "key_fingerprint": "SHA256:a1b2c3d4...",
// "key_origin": "client_provided"
// }
// Note: No private_key_jwk returned — you already have it locally
const { did } = identity
02

Request Challenge

Before each authentication session, request a one-time challenge nonce from the server. The nonce is tied to your DID and expires in 60 seconds. You must sign and submit it before it expires.

challenge.js
// POST https://auth.bysigil.com/v1/auth/challenge
// Request a one-time challenge nonce tied to your DID.
const res = await fetch("https://auth.bysigil.com/v1/auth/challenge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ did }),
})
const challenge = await res.json()
// Response 201:
// {
// "challenge_id": "ch_...",
// "nonce": "hex-encoded-random-bytes",
// "expires_in": 60
// }
//
// The nonce expires in 60 seconds. Sign and verify before it expires.
const { challenge_id, nonce } = challenge
03

Sign Nonce & Verify

Sign the nonce as a UTF-8 string using your Ed25519 private key (use the SDK's signChallenge helper), then POST the signature to /v1/auth/verify. On success, you receive a session token and a fresh Verifiable Credential valid for 24 hours.

verify.js
// Sign the nonce with your Ed25519 private key, then verify.
// CRITICAL: Sign the nonce as a UTF-8 string (NOT hex-decoded bytes).
// Using the SDK (recommended):
const signature = await AuthAgents.signChallenge(keyPair.privateKeyJwk, nonce)
// Or manually with Web Crypto API:
// const key = await crypto.subtle.importKey(
// "jwk", keyPair.privateKeyJwk, { name: "Ed25519" }, false, ["sign"]
// )
// const raw = await crypto.subtle.sign("Ed25519", key, new TextEncoder().encode(nonce))
// const signature = btoa(String.fromCharCode(...new Uint8Array(raw)))
// .replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
// POST /v1/auth/verify with the challenge_id, did, and signature
const res = await fetch("https://auth.bysigil.com/v1/auth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ challenge_id, did, signature }),
})
const result = await res.json()
// Response 200:
// {
// "valid": true,
// "session_token": "sess_...",
// "credential": "eyJhbGciOiJFZERTQSJ9...",
// "agent": {
// "did": "did:key:z6Mk...",
// "agent_name": "Claude",
// "agent_model": "claude-opus-4-6",
// "agent_provider": "Anthropic",
// "agent_purpose": "Research assistant",
// "key_fingerprint": "SHA256:a1b2c3d4..."
// },
// "expires_in": 3600
// }
04

Present Credential to Websites

Include the VC-JWT credential in your requests to websites that accept Agent Auth. The site verifies it with one API call and gets your complete verified identity, including key_origin indicating whether your key was server-generated or self-provided.

present.js
// The credential is a VC-JWT you can present to any website.
// Include it in your API requests or HTTP headers.
// Option A — Authorization header
fetch("https://yoursite.com/api/endpoint", {
headers: {
"Authorization": `Bearer ${credential}`,
"X-Agent-DID": did,
},
})
// Option B — Request body
fetch("https://yoursite.com/api/endpoint", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ credential, did, ...yourPayload }),
})
// The site verifies by calling Agent Auth:
// POST https://auth.bysigil.com/v1/credentials/verify
// { "credential": "eyJ..." }
//
// Response: { "valid": true, "did": "...", "agent_name": "Claude",
// "key_origin": "server_generated", ... }
//
// Credentials (VC-JWT) last 24 hours. Sessions last 1 hour.
// Re-authenticate via challenge-response to get a fresh credential.

// USE THE SDK

The Node.js and Python SDKs include generateKeyPair() and signChallenge() helpers for the headless flow. See the SDK section below.

VIEW SDK DOCS ↓
// SDK

Integrate with your SDK of choice.

Official SDKs for Node.js and Python. For AI agents: use generateKeyPair() and signChallenge() for BYOK headless authentication. For website backends: verify agent credentials in a single call with verify().

The headless examples show the complete BYOK agent-side authentication flow (recommended). The verification examples show how websites verify agent credentials.

Install
npm install auth-agents

BYOK Headless Agent Flow (recommended)

agent.ts
// Complete BYOK headless flow — recommended for AI agents.
// Generate your own keypair. Your private key never leaves your environment.
import { AuthAgents } from "auth-agents"
const authAgents = new AuthAgents()
// Step 1: Generate your own Ed25519 keypair (BYOK)
const keyPair = await AuthAgents.generateKeyPair()
// keyPair.publicKeyJwk — send this to Agent Auth during registration
// keyPair.privateKeyJwk — keep this secret, never share it
// Step 2: Register your identity with your public key
const identity = await authAgents.register({
agent_name: "Claude",
agent_model: "claude-opus-4-6",
agent_provider: "Anthropic",
agent_purpose: "Research assistant",
public_key_jwk: keyPair.publicKeyJwk,
})
// identity.did — "did:key:z6Mk..."
// identity.credential — "eyJhbGciOiJFZERTQSJ9..."
// identity.key_origin — "client_provided" (BYOK)
// Step 3: Request a challenge (repeat when credential expires)
const challenge = await authAgents.challenge(identity.did)
// Step 4: Sign the challenge nonce and authenticate
// CRITICAL: The nonce is signed as UTF-8 text, NOT hex-decoded bytes
const signature = await AuthAgents.signChallenge(keyPair.privateKeyJwk, challenge.nonce)
const session = await authAgents.authenticate({
challenge_id: challenge.challenge_id,
did: identity.did,
signature,
})
// session.credential — fresh VC-JWT (valid 24 hours)
// session.session_token — "sess_..."
// Step 5: Present the credential to websites
fetch("https://yoursite.com/api/task", {
headers: { "Authorization": `Bearer ${session.credential}` },
})

Website Backend — Next.js API route

app/api/auth/agent/route.ts
// app/api/auth/agent/route.ts
// Your backend callback handler — receives credential from the callback page.
import { AuthAgents } from "auth-agents"
const authAgents = new AuthAgents()
export async function POST(request: Request) {
const { credential } = await request.json()
// One call to verify the agent's credential
const result = await authAgents.verify(credential)
if (!result.valid) {
return Response.json({ error: result.message }, { status: 401 })
}
// result.did — "did:key:z6Mk..."
// result.agent_name — "Claude"
// result.agent_model — "claude-opus-4-6"
// result.agent_provider — "Anthropic"
// result.agent_purpose — "Research assistant"
// result.key_fingerprint — "SHA256:4fee8cb539c1..."
// result.key_origin — "server_generated" | "client_provided"
// result.issued_at — "2026-02-26T01:58:15.000Z"
// result.expires_at — "2026-02-27T01:58:15.000Z"
// Create a session in your database
const sessionId = crypto.randomUUID()
await db.insert("sessions", {
session_id: sessionId,
did: result.did,
agent_name: result.agent_name,
agent_model: result.agent_model,
expires_at: result.expires_at,
})
return Response.json({ authenticated: true, session_id: sessionId })
}
Express.js example →
server.ts
// Express.js example
import express from "express"
import { AuthAgents } from "auth-agents"
const app = express()
const authAgents = new AuthAgents()
app.post("/auth/agent", express.json(), async (req, res) => {
const { credential } = req.body
const result = await authAgents.verify(credential)
if (!result.valid) {
return res.status(401).json({ error: result.message })
}
// Agent verified — create session and grant access
req.session.agent = {
did: result.did,
name: result.agent_name,
model: result.agent_model,
provider: result.agent_provider,
}
res.json({ authenticated: true, agent_name: result.agent_name })
})
Install
pip install auth-agents

BYOK Headless Agent Flow (recommended)

agent.py
# Complete BYOK headless flow — recommended for AI agents.
# Generate your own keypair. Your private key never leaves your environment.
from auth_agents import AuthAgents
auth = AuthAgents()
# Step 1: Generate your own Ed25519 keypair (BYOK)
key_pair = AuthAgents.generate_key_pair()
# key_pair["public_key_jwk"] — send this to Agent Auth during registration
# key_pair["private_key_jwk"] — keep this secret, never share it
# Step 2: Register your identity with your public key
identity = auth.register(
agent_name="Claude",
agent_model="claude-opus-4-6",
agent_provider="Anthropic",
agent_purpose="Research assistant",
public_key_jwk=key_pair["public_key_jwk"],
)
# identity["did"] — "did:key:z6Mk..."
# identity["credential"] — "eyJhbGciOiJFZERTQSJ9..."
# identity["key_origin"] — "client_provided" (BYOK)
# Step 3: Request a challenge (repeat when credential expires)
challenge = auth.challenge(identity["did"])
# Step 4: Sign the challenge nonce and authenticate
# CRITICAL: The nonce is signed as UTF-8 text, NOT hex-decoded bytes
signature = AuthAgents.sign_challenge(key_pair["private_key_jwk"], challenge["nonce"])
session = auth.authenticate(
challenge_id=challenge["challenge_id"],
did=identity["did"],
signature=signature,
)
# session["credential"] — fresh VC-JWT (valid 24 hours)
# session["session_token"] — "sess_..."
# Step 5: Present the credential to websites
import requests
requests.get(
"https://yoursite.com/api/task",
headers={"Authorization": f"Bearer {session['credential']}"},
)

Website Backend — FastAPI route

app/auth.py
# FastAPI example — app/auth.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from auth_agents import AuthAgents
app = FastAPI()
auth_agents = AuthAgents()
class CallbackRequest(BaseModel):
credential: str
@app.post("/auth/agent")
async def verify_agent(body: CallbackRequest):
result = auth_agents.verify(body.credential)
if not result["valid"]:
raise HTTPException(status_code=401, detail=result["message"])
# result["did"] — "did:key:z6Mk..."
# result["agent_name"] — "Claude"
# result["agent_model"] — "claude-opus-4-6"
# result["agent_provider"] — "Anthropic"
# result["agent_purpose"] — "Research assistant"
# result["key_fingerprint"] — "SHA256:4fee8cb539c1..."
# result["key_origin"] — "server_generated" | "client_provided"
# result["issued_at"] — "2026-02-26T01:58:15.000Z"
# result["expires_at"] — "2026-02-27T01:58:15.000Z"
# Create a session in your database
session = create_session(
did=result["did"],
agent_name=result["agent_name"],
agent_model=result["agent_model"],
expires_at=result["expires_at"],
)
return {"authenticated": True, "session_id": session.id}
Flask example →
app.py
# Flask example — app.py
from flask import Flask, request, jsonify
from auth_agents import AuthAgents
app = Flask(__name__)
auth_agents = AuthAgents()
@app.post("/auth/agent")
def verify_agent():
credential = request.json.get("credential")
result = auth_agents.verify(credential)
if not result["valid"]:
return jsonify(error=result["message"]), 401
# Agent verified — create session and grant access
session["agent"] = {
"did": result["did"],
"name": result["agent_name"],
"model": result["agent_model"],
"provider": result["agent_provider"],
}
return jsonify(authenticated=True, agent_name=result["agent_name"])