Authentication
Every tool call (HTTP, MCP and A2A) needs a client ID and secret. Agents register themselves; people use a short form. Credentials last 30 days and renew with one call.
1. Get credentials
Agents register without any prior credentials, using either of these:
- A2A: read
/.well-known/agent-card.json, then sendSendMessagetohttps://util.caruana.me/a2awith a data part{"skill":"register","registration":{…}}. See A2A. - OAuth Dynamic Client Registration (RFC 7591):
POST /api/auth/register. Metadata is at/.well-known/oauth-authorization-server.
curl -X POST https://util.caruana.me/api/auth/register \
-H 'content-type: application/json' \
-d '{"client_name":"Release checker","contacts":["[email protected]"],"software_id":"example-release-checker","software_version":"2.1.0","grant_types":["client_credentials"],"agent":{"name":"Release checker","description":"Verifies downloaded release artifacts before deployment.","purpose":"Verify SHA-256 checksums of release artifacts and decode their signing certificates.","on_behalf_of":{"name":"Example Corp platform team","type":"organization","contact":"[email protected]"},"runtime":{"model_provider":"Anthropic","model":"claude-sonnet-5-5","framework":"Claude Agent SDK"},"hosting":{"provider":"Cloudflare Workers","country":"US"},"instructions":"Before each deployment, verify every artifact against the published SHA256SUMS and report mismatches.","autonomy":"supervised","intended_operations":["hash_verify","cert_decode"],"expected_volume":"about 50 calls/day","identity":{"agent_card_url":"https://agents.example.com/.well-known/agent-card.json","domain":"example.com"}}}' People use the credentials form, which includes a Cloudflare Turnstile human check.
The response contains client_id (cu_id_…) and client_secret (cu_sk_…).
The secret is shown once: only its SHA-256 hash is stored. Both are
128/256-bit random values from the platform CSPRNG.
2. Call the API
Send the credentials with every request, in one of these two ways:
# HTTP Basic (RFC 7617): simplest
curl -u "$CLIENT_ID:$CLIENT_SECRET" 'https://util.caruana.me/api/hash?algorithm=sha256&input=hello'
# Or exchange them for a 1-hour bearer token (OAuth 2.0 client credentials, RFC 6749 §4.4)
curl -X POST https://util.caruana.me/api/auth/token -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=client_credentials
# {"access_token":"cu_at_…","token_type":"Bearer","expires_in":3600}
curl -H "Authorization: Bearer $TOKEN" 'https://util.caruana.me/api/hash?algorithm=sha256&input=hello' MCP clients: configure the header, e.g.
claude mcp add --transport http caruana-utilities https://util.caruana.me/mcp --header "Authorization: Basic <base64 of id:secret>"
. Listing tools works without credentials; calling them does not.
3. Expiry and renewal
- New credentials are valid for 30 days.
-
POST /api/auth/renewwith the same credentials sets expiry to 30 days from now. A longer expiry is never shortened. - Expired credentials can still be renewed for 90 days after they expire. After that, renewal is refused and you register again.
- Every creation, renewal, refused renewal, token issue and use of expired credentials is recorded in the client's event history.
-
GET /api/auth/whoamishows your record, expiry, recent events and a 30-day usage summary: everything that is stored about you.
curl -X POST https://util.caruana.me/api/auth/renew -u "$CLIENT_ID:$CLIENT_SECRET"
# {"client_id":"cu_id_…","renewed":true,"previous_expires_at":"…","expires_at":"…","renewable_until":"…","was_expired":false} 4. Limits
- Per client: 60 requests per minute, counted over
the last 60 seconds. Exceeding it returns
429 rate_limitedwithRetry-After: 60. - Per IP: a Cloudflare rate-limiting rule protects
/api/*,/mcpand/a2a, including registration.
5. Authentication errors
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthenticated |
No credentials, wrong credentials, or an expired bearer token. The response carries WWW-Authenticate.
|
| 401 | credentials_expired | The client has expired. The message says whether it can still be renewed, and until when. |
| 403 | revoked | The owner revoked these credentials. |
| 403 | renewal_refused | More than 90 days past expiry, or revoked. |
| 429 | rate_limited | Too many requests in the last minute. |
Registration document
Only client_name and agent.purpose are required, but please tell us
as much as you can. The response includes a profile_completeness score and lists
the fields you left out. Identity claims are recorded as given; this server makes no outbound
requests to verify them. Fields we especially value: agent.name, agent.description, agent.on_behalf_of.name, agent.on_behalf_of.type, agent.on_behalf_of.contact, agent.operator.name, agent.runtime.model_provider, agent.runtime.model, agent.runtime.framework, agent.hosting.provider, agent.hosting.country, agent.instructions, agent.autonomy, agent.intended_operations, agent.identity.agent_card_url, contacts.
| Field | Type | Description |
|---|---|---|
client_name | string | Display name for this client. |
client_uri optional | string | Home page of the client. |
logo_uri optional | string | Logo URL (displayed only to the service owner). |
contacts optional | array of string | Email addresses of people responsible for this client. |
tos_uri optional | string | Terms of service of the client. |
policy_uri optional | string | Privacy policy of the client. |
software_id optional | string | Stable identifier of the client software. |
software_version optional | string | Version of the client software. |
grant_types optional | array of "client_credentials" | Only client_credentials is supported. |
token_endpoint_auth_method optional | "client_secret_basic" | "client_secret_post" | How you will authenticate at the token endpoint. |
agent | object | Everything you are able to tell us about the agent. Only purpose is required; every other field helps us trust and support you. |
agent.name optional | string | The agent's own name. |
agent.description optional | string | What the agent is. |
agent.version optional | string | Agent version. |
agent.purpose | string | What you will use these tools for. |
agent.on_behalf_of optional | object | The person or organization the agent acts for. |
agent.on_behalf_of.name optional | string | Person or organization name. |
agent.on_behalf_of.type optional | "person" | "organization" | "agent" | "unknown" | What kind of party this is. |
agent.on_behalf_of.contact optional | string | Email address or other contact for this party. |
agent.on_behalf_of.url optional | string | Website or profile URL. |
agent.operator optional | object | Who builds or runs the agent, if different. |
agent.operator.name optional | string | Person or organization name. |
agent.operator.type optional | "person" | "organization" | "agent" | "unknown" | What kind of party this is. |
agent.operator.contact optional | string | Email address or other contact for this party. |
agent.operator.url optional | string | Website or profile URL. |
agent.runtime optional | object | What the agent runs on. |
agent.runtime.model_provider optional | string | e.g. Anthropic, OpenAI, Google, self-hosted. |
agent.runtime.model optional | string | Model name/version. |
agent.runtime.framework optional | string | Agent framework, e.g. Claude Agent SDK, LangGraph. |
agent.runtime.framework_version optional | string | Framework version. |
agent.runtime.platform optional | string | Host platform, e.g. Claude Code, a cloud function. |
agent.hosting optional | object | Where the agent runs. |
agent.hosting.provider optional | string | Cloud or hosting provider. |
agent.hosting.region optional | string | Region or data centre. |
agent.hosting.country optional | string | Country (ISO 3166 code or name). |
agent.instructions optional | string | A summary (or the full text) of the agent's instructions or system prompt, as far as you are willing to share. |
agent.autonomy optional | "human-in-the-loop" | "supervised" | "autonomous" | "unknown" | How much human oversight the agent operates under. |
agent.intended_operations optional | array of string | Operation IDs you expect to call, e.g. ["hash", "time_convert"]. |
agent.expected_volume optional | string | Rough call volume, e.g. "about 200 calls/day". |
agent.identity optional | object | Identity claims. They are recorded as given and not verified (this server makes no outbound requests to check them). |
agent.identity.agent_card_url optional | string | Your own A2A agent card. |
agent.identity.did optional | string | Decentralized identifier, e.g. did:web:example.com. |
agent.identity.spiffe_id optional | string | SPIFFE ID, e.g. spiffe://example.org/agent. |
agent.identity.domain optional | string | A domain you control. |
agent.identity.public_key_jwk optional | object | A PUBLIC key (JWK) you may sign with. Never send a private key. |
agent.identity.other optional | string | Any other identifier. |
agent.notes optional | string | Anything else we should know. |