Build Your Own Contextual Access Server
This guide walks through implementing a webhook server that satisfies the Contextual Access Webhook contract. You can write it in any language — the contract is defined by an OpenAPI 3.0 spec.
OpenAPI spec
The canonical contract is maintained in the ArcadeAI/schemas repository:
- OpenAPI 3.0 spec: logic_extensions/http/1.0/schema.yaml
You can browse it interactively on the API Reference page.
Generate server stubs
Use the OpenAPI spec with standard code generators to scaffold your server:
| Language | Generator |
|---|---|
| Go | oapi-codegen , ogen |
| Python | openapi-generator , datamodel-code-generator |
| TypeScript | openapi-typescript , openapi-generator |
Example (Go with oapi-codegen):
curl -sL https://raw.githubusercontent.com/ArcadeAI/schemas/main/logic_extensions/http/1.0/schema.yaml -o schema.yaml
oapi-codegen -generate types,server -package server schema.yamlThe logic-extensions-examples repo uses this approach — see pkg/server/ for the generated types.
Endpoints to implement
Your server needs to expose HTTP POST endpoints for each hook point you want to handle. Endpoint paths are fully configurable in the Dashboard — the defaults are shown below.
| Endpoint | Hook point | Required |
|---|---|---|
POST /access | Access Hook | Only if you need tool access control |
POST /pre | Pre-Execution Hook | Only if you need tool request validation/modification |
POST /post | Post-Execution Hook | Only if you need to filter or modify the result (output and content blocks) |
You do not need to implement all endpoints. Only implement the ones you configure in the Dashboard.
Pre-Execution Hook
When: Before each .
Request (Arcade sends to your server)
| Field | Type | Description |
|---|---|---|
execution_id | string | Correlates request/response across hooks |
tool | object | name, toolkit, version, and optional metadata (see Tool Metadata) |
inputs | object | Tool inputs (name → value) |
context | object | authorization (OAuth per provider), secrets (key names only), metadata, user_id |
Response (your server returns)
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | OK, CHECK_FAILED, or RATE_LIMIT_EXCEEDED |
error_message | string | No | Shown to the agent when denying |
override | object | No | inputs and/or secrets to modify the request |
Post-Execution Hook
When: After .
Request
| Field | Type | Description |
|---|---|---|
execution_id | string | Correlates with pre-execution hook |
tool | object | name, toolkit, version, and optional metadata (see Tool Metadata) |
inputs | object | Tool inputs |
success | boolean | Whether the tool call succeeded |
output | any | The execution output (any JSON type) |
content | array | Content blocks returned alongside output (see Content blocks) |
execution_code | string | Status from worker |
execution_error | string | Error message from tool call |
context | object | Same as pre-execution hook |
Response
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | OK, CHECK_FAILED, or RATE_LIMIT_EXCEEDED |
error_message | string | No | Shown to the agent when denying |
override | object | No | output and/or content to replace what’s returned to the agent |
Content blocks
content holds the blocks a remote server returned alongside its result, in Arcade’s content-block format, which follows MCP’s. It’s absent when the server sent no blocks, and on errors. An empty override.content list removes every block, and clients then get output as text.
If your hook redacts output, redact content too. Otherwise the server’s text reaches the client unchanged.
Access Hook
When: When Arcade needs to know which a can see. Supports batch requests.
Request
| Field | Type | Description |
|---|---|---|
user_id | string | User to check |
toolkits | object | Map of toolkit name → toolkit info. Each toolkit contains a tools map of tool name → version info arrays. Each version info includes version, requirements, and optional metadata (see Tool Metadata) |
Response
Return either an allow list or a deny list (not both):
| Field | Type | Description |
|---|---|---|
only | object | If present, only these tools are allowed (deny list ignored) |
deny | object | Tools listed here are denied (ignored if only is present) |
If you return neither field, the Arcade treats it as “no change”—all remain allowed at this hook.
Tool Metadata
can include an optional metadata field with classification, behavioral, and custom information. Webhook servers can use this metadata to make more granular access and filtering decisions.
All metadata fields are optional and omitted from the JSON when empty, so existing webhook servers continue to work without changes.
For the full list of valid service_domains and operations values, see Add Tool Metadata.
Structure
| Field | Type | Description |
|---|---|---|
metadata | object | absent | Container for tool metadata. Omitted when the tool has no metadata set. |
metadata.classification | object | Classification information. |
metadata.classification.service_domains | string[] | Service domains this tool interfaces with (for example, "email", "calendar", "crm"). |
metadata.behavior | object | Behavioral characteristics. |
metadata.behavior.operations | string[] | Operations this tool performs (for example, "read", "create", "update", "delete", "opaque"). |
metadata.behavior.read_only | boolean | Whether the tool only reads data. |
metadata.behavior.destructive | boolean | Whether the tool can delete or irreversibly modify data. |
metadata.behavior.idempotent | boolean | Whether repeated calls with the same inputs produce the same result. |
metadata.behavior.open_world | boolean | Whether the tool can affect state outside its defined outputs. |
metadata.extras | object | Arbitrary additional metadata set by the toolkit author (for example, {"IdP": "entra_id"}). |
Example: pre/post hook request with metadata
{
"execution_id": "exec_abc123",
"tool": {
"name": "ListEmails",
"toolkit": "Gmail",
"version": "1.0.0",
"metadata": {
"classification": {
"service_domains": ["email"]
},
"behavior": {
"operations": ["read"],
"read_only": true,
"destructive": false,
"idempotent": true,
"open_world": false
},
"extras": {
"IdP": "entra_id"
}
}
},
"inputs": { "query": "from:your-manager" },
"context": { "user_id": "user_123" }
}Example: access hook request with metadata
{
"user_id": "user_123",
"toolkits": {
"Gmail": {
"tools": {
"ListEmails": [
{
"version": "1.0.0",
"metadata": {
"classification": {
"service_domains": ["email"]
},
"behavior": {
"operations": ["read"],
"read_only": true
}
},
"requirements": {
"authorization": [{ "provider_type": "oauth2" }]
}
}
]
}
}
}
}Use cases
Webhook servers can use metadata to make more granular decisions:
- Block destructive operations—Deny where
metadata.behavior.destructiveistruefor certain - Restrict by service domain—Only allow in the
"email"domain for a particular - Audit read vs. write—Log or rate-limit based on their
operationsarray - Route by IdP—Use
metadata.extras.IdPto apply provider-specific policies
Response codes (pre and post)
| Code | Meaning |
|---|---|
OK | Proceed; optional override is applied |
CHECK_FAILED | Deny the operation; error_message is shown to the agent |
RATE_LIMIT_EXCEEDED | Deny with rate-limit semantics |
For per- call caps that need no webhook server, Arcade also enforces built-in rate limits.
Authentication
Arcade sends one of:
- Bearer:
Authorization: Bearer <token>(token from extension config) - mTLS: Client certificate during TLS handshake; optional CA cert for server verification
Configure the auth method when creating your extension in the Dashboard.
Failure handling and retries
- Timeout: Configurable per extension (default 5s); can be overridden per hook. On timeout, the hook’s failure mode applies (Fail closed = block, Fail open = allow).
- Retries: Optional at the extension level; only for transient failures (5xx, timeout, connection error). 4xx responses are not retried.
Next steps
- API Reference — Interactive Swagger documentation for the full schema
- Run an extension — Try the open-source example servers as reference
- How Hooks Work — Understand execution order, phases, and failure modes