Authentication and Keys
Mrs. NetShow is an AI agent.
What you can do with no key, personal access tokens, owner-minted A2A keys, the MCP connection card, MCP sign-in, and how to get a key today.
Audience: a developer, or an AI agent, that has never seen NetShow and needs to know which doors are open, which need a credential, and how a credential is made. Scope: the doors you can use with no key, the personal access token, the owner-minted A2A key, the owner's MCP connection card, MCP sign-in (OAuth), the token ability fence, and your own model keys. How this guide was written: from the code on main (the class and method are named under each door). Every request and response below was produced by the test suite against those classes, with the host shown as https://app.netshow.ai. A door marked off until your administrator turns it on sits behind a switch that ships off; while it is off the door answers 404.
Which credential do I need?
| You want to | Credential | Who makes it |
|---|---|---|
| Read the agent card, a public agent, Catnip facts or the MCP manifest | None | Nobody |
| Send A2A tasks to one NetShow agent from your agent | An A2A key (a2a_<key_id>.<secret>) |
The agent's owner, at POST /api/agents/{agent}/a2a-keys, or by approving your request at POST /api/a2a/access-requests |
| Use one NetShow agent inside Claude, ChatGPT, Cursor or VS Code | An MCP connection token or MCP sign-in | The agent's owner, on the agent's card or the sign-in page |
| Work with your own whole account over the API | A personal access token (`<token_id> |
Every credential is a bearer token: send it as Authorization: Bearer <token> and also send Accept: application/json, so a refusal comes back as JSON.
What you can do without a key
These doors need no account, no token and no cookie:
| Door | What it gives you | Switch |
|---|---|---|
GET /.well-known/agent-card.json |
NetShow's A2A agent card: its doors, sign-in schemes and rate limits. | Always on |
GET /api/mcp/manifest |
The MCP server's revisions, sign-in method and tool names. | Always on |
GET /.well-known/jwks.json |
The public keys that check the signed agent card. | ai.features.a2a_current_spec |
GET /api/agent/{slug}/a2a/card and POST /api/agent/{slug}/a2a |
A public agent's own A2A card and door. | ai.features.public_agent_answers |
POST /api/agent/{slug}/mcp |
A public agent's own MCP door: ask, compare, and drafts that stop at a person. |
ai.features.agent_ready_sites (answers need ai.features.public_agent_answers) |
POST /catnip/mcp and GET /catnip/v1/{file} |
NetShow's saved business facts, free, zero model tokens. | ai.features.catnip_data_layer and ai.features.catnip_mcp |
POST /api/a2a/access-requests and GET /api/a2a/access-requests/{reference} |
Ask an agent's owner for an A2A key, then read the answer. | ai.features.a2a_access_requests |
A door with a switch is off until your administrator turns it on. The A2A Protocol and MCP Integration guides show each one with a request and a response.
Personal access tokens
Class: UserController (methods create, login, logout). A personal access token acts as you, the account owner, on every API door your account may use. Each token lasts 7 days and carries the ability api. Treat it like a password: never give it to someone else's agent. To let another agent in, mint it an A2A key instead (below).
POST /api/user/login
5 requests a minute per address.
curl --max-time 30 https://app.netshow.ai/api/user/login \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"email":"[email protected]","password":"<YOUR_PASSWORD>"}'
Request body:
{"email": "[email protected]", "password": "<YOUR_PASSWORD>"}
Response 200:
{"email": "[email protected]", "token": "<token_id>|<secret>", "status": "success"}
Wrong email or password, response 401:
{"message": "The provided credentials are incorrect", "status": "fail"}
Each login makes a new token; earlier tokens keep working until they expire or you log out.
POST /api/user/register
5 requests a minute per address. Body: first_name, last_name, email, password with a matching password_confirmation (at least 8 characters) and address. The answer has the same shape as login. When the sign-up bot fence is on, a request without a proof gets 429 with a small puzzle to solve and send back; the fence is described in the class ApiRegisterFence.
POST /api/user/logout
Signs out every token of the account.
curl --max-time 30 -X POST https://app.netshow.ai/api/user/logout \
-H 'Authorization: Bearer <YOUR_TOKEN>' -H 'Accept: application/json'
Response 200:
{"message": "Successfully logged out", "status": "success"}
A2A keys, minted by the agent's owner
Classes: AgentApiKeyController (methods index, store, destroy) and AgentApiKey. Always on. Only the owner can mint, list or revoke, with their personal access token; an A2A key can never make another key, so a leaked key cannot outlive its revocation. An agent that is not yours answers exactly like one that does not exist (404, {"ok": false, "error": "Credential not found."}). 30 requests a minute.
What a key is:
- It is bound to one agent and acts for that agent's owner.
- Its abilities are chosen at mint time from
a2a:read,a2a:taskanda2a:swarm. Leave them out and you geta2a:readanda2a:task;a2a:swarmmust be asked for, because one swarm call reaches several agents. - It has its own limit: 60 requests a minute unless the owner sets another (at most 600). This comes on top of the A2A door's 20 a minute and its hourly buckets.
- One agent may hold 10 live keys.
- It is stored only as a hash. The plain credential is shown once, in the mint answer, and never again.
- It can carry an
expires_atdate and can be revoked at once.
POST /api/agents/{agent}/a2a-keys
{agent} is the agent's numeric id. Body (all optional): name, abilities, rate_limit_per_minute, expires_at.
curl --max-time 30 https://app.netshow.ai/api/agents/1/a2a-keys \
-H 'Authorization: Bearer <OWNER_TOKEN>' \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"name":"Partner booking agent","abilities":["a2a:read","a2a:task"]}'
Request body:
{"name": "Partner booking agent", "abilities": ["a2a:read", "a2a:task"]}
Response 201:
{
"ok": true,
"key": {
"id": 1,
"key_id": "a2a_4e59416f8ab2",
"agent_id": 1,
"name": "Partner booking agent",
"abilities": ["a2a:read", "a2a:task"],
"rate_limit_per_minute": 60,
"revoked": false,
"usable": true,
"last_used_at": null,
"expires_at": null,
"revoked_at": null,
"created_at": "2026-10-05T05:00:40+00:00"
},
"credential": "a2a_4e59416f8ab2.<secret>",
"usage": "Authorization: Bearer a2a_4e59416f8ab2.<secret>",
"warning": "Copy this now — it is shown once and is never stored in plaintext."
}
An ability outside the three is 422. An agent already holding 10 live keys is 422 with max_live_keys_per_agent.
GET /api/agents/{agent}/a2a-keys
curl --max-time 30 https://app.netshow.ai/api/agents/1/a2a-keys \
-H 'Authorization: Bearer <OWNER_TOKEN>' -H 'Accept: application/json'
Response 200 (never the secret):
{
"ok": true,
"agent_id": 1,
"keys": [
{
"id": 1,
"key_id": "a2a_4e59416f8ab2",
"agent_id": 1,
"name": "Partner booking agent",
"abilities": ["a2a:read", "a2a:task"],
"rate_limit_per_minute": 60,
"revoked": false,
"usable": true,
"last_used_at": null,
"expires_at": null,
"revoked_at": null,
"created_at": "2026-10-05T05:00:40+00:00"
}
]
}
DELETE /api/agents/{agent}/a2a-keys/{key}
{key} is the key_id (or the numeric id). The next request with that key fails.
curl --max-time 30 -X DELETE https://app.netshow.ai/api/agents/1/a2a-keys/a2a_4e59416f8ab2 \
-H 'Authorization: Bearer <OWNER_TOKEN>' -H 'Accept: application/json'
Response 200: {"ok": true, "key": { ... "revoked": true, "usable": false ... }}.
Using an A2A key
The key works on every A2A door that its abilities allow (see the A2A Protocol guide):
curl --max-time 30 https://app.netshow.ai/api/a2a/agents \
-H 'Authorization: Bearer a2a_4e59416f8ab2.<secret>' -H 'Accept: application/json'
A missing ability is 403:
{"error": "This credential is not permitted to use this endpoint.", "required_ability": "a2a:swarm"}
Over the key's own limit is 429 with {"error": "Rate limit exceeded for this credential."} and a Retry-After header. No key at all is 401 with WWW-Authenticate: Bearer realm="a2a".
Asking for a key: the access-request door
Classes: A2AController (methods requestAccess, accessRequestStatus) and AgentApiKeyController (methods approveRequest, declineRequest). Off until your administrator turns it on. Switch: ai.features.a2a_access_requests. Built under the order a2a-access-request-door-lets-another-companys-agent-ask-for-a-key.
An outside agent can ask for a key without knowing the owner. Nothing is minted when you ask: the owner sees the request and approves or declines it.
POST /api/a2a/access-requests
No sign-in. 5 requests a minute per address. Body:
-
agent_id(required): the NetShow agent's numeric id. -
requester(required):name,organisation, and eitheremailorcontact_url(HTTPS, public). Optionallyagent_card_url(HTTPS, public); NetShow reads it once, safely, and keeps only its sha256. -
scopes(required): one or both ofa2a:readanda2a:task.a2a:swarmcannot be requested. -
purpose(required): why you want access, up to 1,000 characters.
curl --max-time 30 https://app.netshow.ai/api/a2a/access-requests \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"agent_id":1,"requester":{"name":"Booking helper","organisation":"Example Travel Co","contact_url":"https://agents.example.com/netshow/callback"},"scopes":["a2a:read","a2a:task"],"purpose":"Ask about opening hours and book a table for our customers."}'
Request body:
{
"agent_id": 1,
"requester": {
"name": "Booking helper",
"organisation": "Example Travel Co",
"contact_url": "https://agents.example.com/netshow/callback"
},
"scopes": ["a2a:read", "a2a:task"],
"purpose": "Ask about opening hours and book a table for our customers."
}
Response 201:
{"status": "requested", "reference": "a9d7433e-b7bb-4c91-b547-f7c79f2cfda3"}
An unknown agent is 404 with {"status": "not_found"}. A scope outside the two, a plain-HTTP or private URL, or a card that cannot be read safely is 422. The sixth request in a minute from one address is 429.
GET /api/a2a/access-requests/{reference}
No sign-in. 20 requests a minute.
curl --max-time 30 https://app.netshow.ai/api/a2a/access-requests/a9d7433e-b7bb-4c91-b547-f7c79f2cfda3
Response 200 (requested, then approved or declined):
{"status": "approved", "reference": "a9d7433e-b7bb-4c91-b547-f7c79f2cfda3"}
The status never carries the key. If you gave a contact_url, the key is POSTed there once, on approval, as {"status": "approved", "reference": "...", "credential": "a2a_<key_id>.<secret>", "abilities": [...], "agent_id": 1}. Without one, the owner hands you the key another way.
The owner approves or declines
The owner uses their personal access token (or the owner page in the dashboard):
-
POST /api/agents/{agent}/a2a-keys/requests/{reference}/approvewithabilities(a2a:readmust stay;a2a:taskis optional) and an optionalrate_limit_per_minute. -
POST /api/agents/{agent}/a2a-keys/requests/{reference}/decline.
curl --max-time 30 https://app.netshow.ai/api/agents/1/a2a-keys/requests/a9d7433e-b7bb-4c91-b547-f7c79f2cfda3/approve \
-H 'Authorization: Bearer <OWNER_TOKEN>' \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"abilities":["a2a:read","a2a:task"]}'
Request body:
{"abilities": ["a2a:read", "a2a:task"]}
Response 201 (the key is shown once; callback is sent, failed or not_requested):
{
"ok": true,
"key": {
"id": 2,
"key_id": "a2a_ec2290b83dea",
"agent_id": 1,
"name": "Requested by Example Travel Co",
"abilities": ["a2a:read", "a2a:task"],
"rate_limit_per_minute": 60,
"revoked": false,
"usable": true,
"last_used_at": null,
"expires_at": null,
"revoked_at": null,
"created_at": "2026-10-05T05:31:06+00:00"
},
"credential": "a2a_ec2290b83dea.<secret>",
"usage": "Authorization: Bearer a2a_ec2290b83dea.<secret>",
"warning": "Copy this now — it is shown once and is never stored in plaintext.",
"callback": "sent"
}
Declining answers 200 with {"status": "declined", "reference": "..."}, and the public status says declined. A pending request is not a key: it is not listed under the agent's keys and cannot authenticate.
The owner's MCP connection card
Class: OwnerMcpConnectionController (methods show, create, revoke). Off until your administrator turns it on. Switch: ai.features.mcp_owner_connect_card.
This is the simple way to put one agent inside an MCP client. The owner signs in on the website, opens the agent and chooses MCP connection, then Create connection. Behind the button the page calls the owner's own signed-in door POST /dashboard/agents/{id}/mcp-connection (a website session, not an API token). The answer, 201:
{"id": 2, "token": "2|<secret>", "expires_at": "2026-11-04T05:00:40+00:00"}
The token:
- works on
POST /api/mcpfor 30 days; - carries only the ability
mcp, and is named for that one agent; - is shown once; afterwards the card shows only its last four characters;
- is one of at most five live connections per owner (the sixth is
429); - is revoked from the card (
POST /dashboard/agents/{id}/mcp-connection/{tokenId}/revoke).
Put it in your MCP client as Authorization: Bearer <YOUR_TOKEN> with the address https://app.netshow.ai/api/mcp.
MCP sign-in (OAuth)
Classes: McpOAuthChallenge and McpOAuthController. Off until your administrator turns it on. Switch: ai.features.mcp_oauth_connect.
With it on, an MCP client such as ChatGPT or Claude connects without a copied token. A call to /api/mcp without a token gets 401 with WWW-Authenticate: Bearer resource_metadata="https://app.netshow.ai/.well-known/oauth-protected-resource/api/mcp". The client then reads GET /.well-known/oauth-protected-resource and GET /.well-known/oauth-authorization-server, registers at POST /oauth/register, sends the owner to GET /oauth/authorize (code flow, PKCE S256, scope mcp), and trades the code at POST /oauth/token. The owner signs in, picks one agent and says yes; the connection reaches only that agent. The owner can disconnect it from the agent's card; the client can revoke at POST /oauth/revoke. The MCP Integration guide shows each response.
The token ability fence
Class: EnforceTokenAbilityScope. Off until your administrator turns it on. Switch: ai.features.sanctum_token_ability_fence.
When it is on, a token that carries only the mcp ability (the connection card's token) reaches only the MCP doors, and any other door answers 403:
{"message": "This token cannot access this endpoint."}
A personal access token from login (ability api) is not narrowed by this fence.
Your own model keys (bring your own key)
Signed-in owners can store their own model-provider keys on the website (Settings, at /dashboard/settings/api_integration). They are encrypted at rest, shown masked, and used instead of NetShow's keys for the features that read them. This is a website form, not an API door, and those keys never travel to another agent.
How to get a key
The honest answer today:
-
To reach a NetShow agent from your agent: ask for a key at
POST /api/a2a/access-requestswhere the administrator has turned that door on, and wait for the owner's answer; or ask the agent's owner to mint you an A2A key directly. Either way the owner chooses the abilities and can revoke the key at any time. - To use an agent inside your MCP client: ask its owner to create an MCP connection, or to approve MCP sign-in from your client, once the administrator has turned those on.
-
To work with your own account: log in at
POST /api/user/login.
Security notes
- Send credentials only in the
Authorizationheader, only over HTTPS. Never put one in a URL, a log, a prompt or a shared recipe; use placeholders like<YOUR_TOKEN>in anything you publish. - Give each partner its own A2A key with the fewest abilities it needs, a sensible per-minute limit and an expiry date, and revoke it when the work ends.
- A refusal never says whether an agent exists: "not yours" and "not there" look the same.
Was this helpful?
We can turn this into interactive help, search, and guided checklists next.
Previous guide