Endpoints
Complete reference for all webhook endpoints, covering registration, metadata storage, credential access, proxy forwarding, and token refresh.
Your webhook implements eight endpoints in three categories. Token Vault calls six of these with HMAC-signed requests. Five are fully hands-off (TV never sees credentials), and one (/v1/refresh) briefly handles credentials in transit during OAuth refresh (never stored). Agents and browsers call the remaining two directly; Token Vault is not in the request path for credential access.
Token Vault → Webhook Endpoints
These endpoints are called by the Token Vault backend with HMAC-signed requests (except /v1/exchange).
Verify HMAC before processing
Every endpoint in this section (except /v1/exchange) is HMAC-authenticated. Your webhook must verify the X-TokenVault-Signature header before processing any request. Reject with HTTP 401 if the signature does not match. See Authentication.
POST /v1/exchange
Called once during the webhook binding flow. Your webhook receives a one-time code and returns its HMAC secret. This is the only endpoint called without HMAC authentication; the code itself is the authentication.
{
"code": "550e8400-e29b-41d4-a716-446655440000"
}{
"hmacSecret": "<base64-encoded 256-bit secret>",
"webhookId": "wh_abc123",
"version": "1.0.0",
"capabilities": ["storage", "credential", "proxy", "refresh", "store", "tv-refresh"]
}| Capability | Description |
|---|---|
storage | Metadata CRUD via /v1/storage |
credential | Agent credential access via /v1/credential |
proxy | MCP proxy forwarding via /v1/proxy |
refresh | Notify-only refresh via /v1/refresh-notify |
store | Browser-direct storage via /v1/store |
tv-refresh | TV-mediated refresh via /v1/refresh (opt-in, see below) |
Your webhook should:
- Validate the one-time code (must be unused, within 5-minute TTL).
- Generate and store a 256-bit HMAC secret.
- Return the secret and your webhook's capabilities.
- Mark the code as used. Reject future exchange attempts with
410 Gone.
GET|POST /v1/health
Called on-demand when the user checks webhook health from the vault settings page, and periodically by Token Vault to monitor status.
GET requests are unauthenticated, useful for external monitoring, load balancers, and the /bind status page. POST requests are HMAC-signed.
{
"requestId": "req_xyz789abc012"
}{
"status": "healthy",
"version": "1.0.0",
"capabilities": ["storage", "credential", "proxy", "refresh", "store", "tv-refresh"],
"uptime": 86400,
"tokenCount": 5,
"keyConfigured": true
}| Field | Description |
|---|---|
status | healthy, degraded, or unreachable |
version | Your webhook implementation version |
capabilities | Supported endpoint capabilities (see /v1/exchange) |
keyConfigured | Whether the encryption key has been generated |
POST /v1/storage
Called for metadata operations only. Token Vault uses this endpoint to list tokens, manage proxy configurations, write audit events, and store vault settings. This endpoint never returns plaintext credentials. Credential access goes through /v1/credential (direct) or /v1/proxy (proxy). Verify HMAC signature before processing.
List tokens (metadata only)
Token Vault calls this to populate the dashboard token list. The response contains metadata only: service names, token types, and expiry times. No plaintext credentials.
{
"requestId": "req_list_tokens456",
"operation": "list",
"collection": "tokens"
}{
"requestId": "req_list_tokens456",
"items": [
{
"key": "github",
"meta": {
"serviceName": "github",
"tokenType": "JWT",
"expiryTime": 1720003600000,
"createdAt": "2026-02-01T10:00:00Z"
}
}
]
}Each item must include key and a meta object with at least serviceName.
Store a proxy config
{
"requestId": "req_set_proxy789",
"operation": "set",
"collection": "proxy_configs",
"key": "proxy-abc123",
"data": {
"name": "GitHub MCP",
"upstreamUrl": "https://api.github.com/mcp",
"serviceName": "github",
"headerTemplates": { "Authorization": "Bearer ${TOKEN}" }
}
}{ "requestId": "req_set_proxy789", "status": "ok" }Delete an item
{
"requestId": "req_delete_token000",
"operation": "delete",
"collection": "tokens",
"key": "github"
}{ "requestId": "req_delete_token000", "status": "ok" }Batch list (multiple collections)
Fetch multiple collections in a single round trip. Used by the dashboard to load tokens and audit events together.
{
"requestId": "req_batch_list123",
"operation": "list_batch",
"collections": ["tokens", "audit"]
}{
"requestId": "req_batch_list123",
"results": {
"tokens": {
"items": [
{ "key": "github", "meta": { "serviceName": "github", "tokenType": "JWT" } }
]
},
"audit": {
"items": [
{ "key": "2026-02-15T10:30:00Z", "meta": { "event_type": "AGENT_CREDENTIAL_ACCESS" } }
]
}
}
}list_batch vs list
Use list_batch when you need multiple collections at once (e.g., dashboard load). The collection field is ignored; use the collections array instead. Each collection in the response follows the same format as individual list responses.
Collections
| Collection | Operations | Description |
|---|---|---|
tokens | list, set, delete | Encrypted token documents. List returns metadata only, not credentials. |
proxy_configs | get, set, delete | MCP proxy configurations (keyed by proxy ID) |
audit | set, list | Audit event log (keyed by timestamp, newest-first listing) |
vault_config | get, set | Vault settings (single key: settings) |
Audit events
Token Vault writes audit events to the audit collection. Every token access, agent credential retrieval, token refresh, and policy denial is logged here. Your webhook owns this audit trail.
{
"operation": "set",
"collection": "audit",
"key": "2026-02-15T10:30:00Z",
"data": {
"event_type": "AGENT_CREDENTIAL_ACCESS",
"source": "agent",
"service_name": "github",
"agent_id": "agent-abc123",
"client_ip": "203.0.113.42",
"zero_knowledge": true,
"timestamp": "2026-02-15T10:30:00Z"
}
}| event_type | Description |
|---|---|
SECRET_ACCESS | A token was read (direct user or proxy access) |
AGENT_CREDENTIAL_ACCESS | An agent retrieved a credential via the HTTP endpoint |
TOKEN_REFRESH | A token was refreshed (server-side or webhook-delegated) |
POLICY_DENIED | An access request was blocked by an ABAC policy rule |
POST /v1/proxy
Called when an AI agent makes a request through the MCP proxy. Token Vault validates the proxy key and policies, then forwards the request to your webhook with a signed proxy ticket. Your webhook decrypts the credential, injects it into the upstream request headers (replacing ${TOKEN} placeholders), makes the upstream HTTP call, and returns the response. Token Vault never sees the credential. Verify HMAC signature before processing.
{
"requestId": "req_proxy_abc123",
"ticket": "<signed proxy ticket>",
"service": "github",
"upstream": {
"url": "https://api.github.com/mcp",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": "<base64-encoded request body>"
},
"headerTemplates": {
"Authorization": "Bearer ${TOKEN}"
}
}HTTP/1.1 200 OK
Content-Type: application/json
X-Upstream-Status: 200
<upstream response body, passed through verbatim>Raw passthrough, not a JSON envelope
The response is the upstream HTTP response directly: same status code, same Content-Type, same body. Your webhook does not wrap it in a JSON envelope. The optional X-Upstream-Status header provides the upstream status for observability.
Your webhook should:
- Verify the HMAC signature.
- Verify the proxy ticket (HMAC, expiry, nonce, purpose =
"proxy"). - Read the encrypted credential for the given
servicefrom your storage. - Decrypt the credential using your encryption key.
- Replace
${TOKEN}inheaderTemplateswith the decrypted access token. - Make the HTTP request to
upstream.urlwith the merged headers and body. - Return the upstream response to Token Vault as raw HTTP passthrough (same status code, headers, body).
POST /v1/refresh-notify (notify-only)
Called when Token Vault detects a token approaching expiry. Your webhook owns the credential and handles the refresh independently. Token Vault sends metadata (service name, provider hints) but never the credential itself or the clientSecret. Verify HMAC signature before processing.
Use this endpoint for custom OAuth providers where your webhook has its own client credentials configured locally.
{
"requestId": "req_refresh_abc123",
"service": "my-custom-api",
"reason": "token_expiring",
"expiresAt": "2026-02-17T15:30:00Z",
"refreshHint": {
"provider": "my-custom-api",
"tokenUrl": "https://auth.example.com/oauth/token",
"clientId": "your_client_id"
}
}{
"requestId": "req_refresh_abc123",
"status": "refreshed",
"newExpiresAt": "2026-02-17T16:30:00Z"
}Configuring your own OAuth credentials
Token Vault does not send clientSecret in the refresh hint. To use this endpoint for custom providers, configure your webhook with OAuth credentials (client ID + client secret) for each provider, via environment variables or a local config file. When this endpoint is called, look up credentials by the refreshHint.provider name and use them to call the token endpoint.
Your webhook should:
- Verify the HMAC signature.
- Read the encrypted token for the given
servicefrom your storage. - Decrypt the refresh token using your encryption key.
- Look up your own OAuth client credentials for this provider.
- Call the OAuth provider's token endpoint with your credentials to get a fresh access token.
- Encrypt the new tokens and store them back.
- Return an acknowledgement.
Valid status values:
| Status | Meaning |
|---|---|
refreshed | Successfully refreshed the token |
acknowledged | Notification received, refresh will be attempted |
no_token | No token stored for this service |
no_refresh_token | Token exists but has no refresh token |
refresh_failed | OAuth provider returned an error during refresh |
error | Unexpected internal error |
POST /v1/refresh (TV-mediated)
OAuth credential handling
This is the only endpoint where Token Vault briefly handles credential material in transit. Credentials are forwarded to the OAuth provider and the refreshed tokens are sent straight back to your webhook for encryption -- Token Vault never stores them. Your webhook opts in by reporting the "tv-refresh" capability. The webhook is the killswitch: remove it and TV cannot access any credentials.
Called when Token Vault needs to refresh a token for its built-in OAuth providers (Google, GitHub). Unlike /v1/refresh-notify where the webhook handles everything, this enables a two-phase flow where TV performs the OAuth refresh using its own client_secret. Verify HMAC signature before processing.
Capability: Requires "tv-refresh" in your webhook's capabilities list. Without it, TV falls back to /v1/refresh-notify.
Phase 1: Get refresh token
{
"requestId": "req_refresh_abc123",
"action": "get",
"service": "google"
}{
"requestId": "req_refresh_abc123",
"status": "ok",
"refreshToken": "1//0abc_plaintext_refresh_token...",
"meta": {
"serviceName": "google",
"tokenType": "JWT",
"expiryTime": 1720003600000,
"createdAt": "2026-02-01T10:00:00Z"
}
}Phase 2: Update with refreshed tokens
{
"requestId": "req_refresh_abc123",
"action": "update",
"service": "google",
"tokens": {
"accessToken": "ya29.new_access_token...",
"refreshToken": "1//0abc_new_or_same_refresh_token...",
"expiryTime": 1720007200000
}
}{
"requestId": "req_refresh_abc123",
"status": "updated",
"newExpiresAt": "2026-07-03T17:00:00+00:00"
}Your webhook should:
- Verify the HMAC signature for both phases.
- Phase 1 (get): Decrypt the refresh token and return it with metadata.
- Phase 2 (update): Encrypt the new tokens with your key, update the stored document preserving existing metadata (createdAt, tokenType), and return acknowledgement.
Direct Access Endpoints
These endpoints are called directly by agents and browsers. Token Vault is not in the request path. They authenticate with signed tickets (not HMAC headers). Your webhook must verify the ticket's HMAC signature, check expiry, and prevent nonce replay.
Token Vault never calls these endpoints
These endpoints exist so that credentials flow directly between the agent/browser and your webhook. Token Vault issues the signed tickets that authorize the request, but it never sees the credential data. This is what makes Webhook Mode zero-knowledge.
GET /v1/credential
Zero-knowledge credential access. This endpoint is hit when an agent follows a 307 redirect from Token Vault, or when a browser requests a credential directly. Token Vault validates the agent's API key and ABAC policies, generates a signed credential ticket, and redirects the agent here. Your webhook verifies the ticket and returns the decrypted credential. Token Vault never sees the credential.
Also accepts POST with a JSON body for browser-based access.
GET /v1/credential?ticket=<signed-ticket>&service=github{
"ticket": "<signed-ticket>",
"service": "github"
}{
"token": {
"accessToken": "ghp_abc123...",
"refreshToken": "ghr_xyz789...",
"serviceName": "github",
"tokenType": "JWT",
"createdAt": "2026-02-01T10:00:00Z"
}
}Your webhook should:
- Extract
ticketandservicefrom query params (GET) or JSON body (POST). - Verify the ticket: check HMAC signature, expiry, and nonce (prevent replay).
- Verify that the ticket's
svcfield matches the requestedservice. - Read the encrypted credential from your storage.
- Decrypt using your encryption key.
- Return the plaintext credential.
- Set CORS headers if the request comes from a browser (
Originheader present).
Ticket payload
The signed ticket is a base64url-encoded JSON payload with an HMAC-SHA256 signature: {base64url(payload)}.{hex_signature}. The payload contains:
{
"sub": "user-id",
"svc": "github",
"pur": "agent_credential",
"aid": "agent-id",
"iat": 1708000000,
"exp": 1708000060,
"nonce": "random-hex-string"
}| Field | Description |
|---|---|
sub | Vault owner's user ID |
svc | Service name (must match the service parameter) |
pur | Purpose: "agent_credential", "user_reveal", or "browser_credential" |
aid | Agent ID (present for agent requests, absent for user requests) |
iat | Issued-at timestamp (Unix seconds) |
exp | Expiry timestamp (Unix seconds). Tickets are short-lived (60s default). |
nonce | Random hex string. Reject if already seen (replay prevention). |
POST /v1/store
Browser-direct credential storage. When a user adds a token through the dashboard, Token Vault issues a store ticket. The browser sends the plaintext credential directly to your webhook with this ticket. Your webhook verifies the ticket, encrypts the credential with its own AES-256-GCM key, and persists it. Token Vault never sees the plaintext credential.
{
"ticket": "<signed-store-ticket>",
"service": "github",
"tokenData": {
"accessToken": "ghp_abc123...",
"refreshToken": "ghr_xyz789...",
"tokenType": "JWT",
"expiresAt": "2026-02-17T15:30:00Z"
}
}{
"status": "stored",
"service": "github",
"meta": {
"serviceName": "github",
"tokenType": "JWT",
"createdAt": "2026-02-15T10:00:00Z"
}
}Your webhook should:
- Verify the ticket: check HMAC signature, expiry, nonce, and purpose (
"store"). - Verify that the ticket's
svcfield matches theserviceparameter. - Extract the credential data from
tokenData. - Encrypt sensitive fields (
accessToken,refreshToken) with your AES-256-GCM key. - Store the encrypted document.
- Return metadata (not the credential) in the response.
- Set CORS headers, as this endpoint is called from the browser.
CORS required
Both /v1/credential and /v1/store may be called from the browser. Your webhook must handle OPTIONS preflight requests and return appropriate Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers headers.
Optional Helper Endpoints
These endpoints are convenience helpers for webhook operators. Token Vault does not call them. They help with registration and management.
GET /v1/register-url
Generates a one-time registration URL programmatically (for CLI/API usage instead of the /bind web page).
{
"url": "https://tokenvault.uk/vault/webhook-bind?code=550e8400...&webhook_url=aHR0cHM6...&hmac_hash=abc123...",
"code": "550e8400-e29b-41d4-a716-446655440000",
"expiresIn": 300
}GET /bind
A browser-facing HTML page that displays your webhook's status and a "Connect to TokenVault" button. When clicked, it generates a one-time code and redirects the user to Token Vault's bind page.
Query parameters:
?force=1- Generate a new registration code even if the webhook is already bound (re-binding).