WireCanal API Reference
With a single API key, do everything from creating canals to listing, updating, deleting, and fetching access logs. Nearly every operation available in the dashboard (WebUI) can be performed programmatically.
Overview & Supported Plans
Nearly every canal operation available in the dashboard (WebUI) can be performed through a REST API with API-key authentication. All responses are JSON.
- Create / list / inspect / update (forwarding destination & access protection) / delete canals
- Pause / resume publication and fetch access logs
- Fetch wirecanal.json (Agent configuration) — single or multiple at once — and record grouped-operation sets
- Check your own quota (plan, limits, usage) and the list of edges (shards)
The public API (api_access) is a feature of the Pro plan and above. Plan checks are performed both when an API key is issued and when it is used.
Authentication
Authentication is Bearer authentication with an API key (35 characters starting with wk_). Keys are issued from /api-keys in the dashboard (Pro and above; up to 10 keys per user), and are shown only once, right after issuance. The server stores only a sha256 hash, so even if a dump were leaked, the actual key cannot be recovered (if lost: revoke, then reissue). There is a single permission scope: canal:rw (read/write on your own canals).
Authorization: Bearer wk_(your key)Because authentication is Bearer-only, CSRF cannot occur by design, and no CORS headers are emitted (to prevent accidental use from browsers). All operations are owner-scoped: other users' canals return 404 including their very existence (existence is never disclosed).
Each key can carry a usage memo (up to 2000 characters). If you use multiple keys — for CI, for testing, and so on — the list view makes it easy to tell them apart.
Rate Limits
Rate limits are 60 reads/minute and 20 writes/minute (per user). When exceeded, the API returns 429 with Retry-After (seconds). Please wait a while and try again.
Endpoints
The base URL is https://app.wirecanal.com/v1/api. Responses are always JSON, and errors take the form {"error": "<code>"}. To run the samples below, set KEY to the API key you issued.
KEY="wk_(your key)"Check Your Quota
Returns your plan, canal limit, usage count, and the name of the API key in use.
curl -sS -H "Authorization: Bearer $KEY" \
https://app.wirecanal.com/v1/api/me{
"uid": "...",
"plan_key": "premium",
"max_canals": 20,
"used_canals": 3,
"api_key_name": "ci-bot"
}Edge List
The list of hosting edges (shards) you can specify when creating a canal.
curl -sS -H "Authorization: Bearer $KEY" \
https://app.wirecanal.com/v1/api/shards{
"shards": [
{"shard_id": "ja000"},
{"shard_id": "ja001"},
{"shard_id": "ja100"},
{"shard_id": "ja200"},
{"shard_id": "jan000"}
]
}List Canals
Returns all canals you own (each element is the same canal representation as in "Canal details").
curl -sS -H "Authorization: Bearer $KEY" \
https://app.wirecanal.com/v1/api/canals{
"canals": [ /* array of canal representations (see "Canal details" below) */ ]
}Create a Canal
Creates a canal. In addition to the canal representation, the response includes agent_json (the contents of wirecanal.json), so you can wire everything up in a single round trip.
| Parameter | Type | Description | |
|---|---|---|---|
| forward_target | Optional | string | Forwarding destination (host:port; defaults to localhost:3000) |
| type | Optional | string | http (default) / tcp / mcp |
| subdomain | Optional | string | Preferred subdomain (paid plans) |
| custom_domain | Optional | string | Custom domain (Premium) |
| shard | Optional | string | Hosting edge (from the candidates in GET /v1/api/shards) |
| memo | Optional | string | Usage memo (up to 2000 characters; also shown in the dashboard's list and detail views) |
| param_a / param_b / param_c | Optional | string | Extra parameters (up to 512 characters each). An API-only place to store arbitrary attributes, like tags (not shown in the UI) |
curl -sS -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"forward_target":"localhost:8080"}' \
https://app.wirecanal.com/v1/api/canals{
"canal_id": "...",
"type": "http",
"forward_target": "localhost:8080",
"shard": "ja001",
"reserved": false,
"expires_at": null,
"suspended": false,
"user_paused": false,
"agent_group": null,
"memo": null,
"param_a": null,
"param_b": null,
"param_c": null,
"response_timeout_sec": null,
"protection": {
"ip": {"enabled": false, "allow": []},
"basic": {"enabled": false},
"bearer": {"enabled": false, "count": 0},
"country": {"enabled": false},
"schedule": {"enabled": false},
"path": {"enabled": false},
"fail2ban": {"enabled": false},
"stealth": {"enabled": false}
},
"hostname": "ab12cd34.ja001.wirecanal.com",
"url": "https://ab12cd34.ja001.wirecanal.com/",
"agent_json": {"access_key": "ck_...", "forward_target": "localhost:8080"}
}Save the response's agent_json as your wirecanal.json, then just start the Agent — and you are live.
Canal Details
Returns a single canal representation. Secret values (BASIC username/password, actual Bearer tokens) are never returned.
curl -sS -H "Authorization: Bearer $KEY" \
https://app.wirecanal.com/v1/api/canals/{canal_id}{
"canal_id": "...",
"type": "http",
"forward_target": "localhost:8080",
"shard": "ja001",
"reserved": true,
"expires_at": null,
"suspended": false,
"user_paused": false,
"agent_group": null,
"memo": "For the internal sales dashboard",
"param_a": "env=prod",
"param_b": null,
"param_c": null,
"protection": { /* 8 sections (see "Update Forwarding & Protection" below) */ },
"hostname": "myapp.ja001.wirecanal.com",
"url": "https://myapp.ja001.wirecanal.com/"
}Update Forwarding & Access Protection
Updates the forwarding destination and access protection. Protection uses a per-section shallow merge over its 8 sections (only the sections you write are replaced).
| Parameter | Type | Description | |
|---|---|---|---|
| forward_target | Optional | string | Change the forwarding destination |
| protection | Optional | object | 8 sections (ip / basic / bearer / country / schedule / path / fail2ban / stealth) |
| memo | Optional | string | Update the usage memo (up to 2000 characters; null or an empty string clears it, omitting leaves it unchanged) |
| param_a / param_b / param_c | Optional | string | Update the extra parameters (up to 512 characters each; null or an empty string clears it, omitting leaves it unchanged) |
| response_timeout_sec | Optional | integer | Override the response wait time (seconds, 1–3600; null restores the 120-second default). This is how long to wait for the forwarding destination to start responding; extend it only for canals that expose slow synchronous APIs. Paid plans only (specifying it on the free plan returns 403 plan_required); HTTP / MCP canals only (specifying it on TCP returns 400 bad_timeout). Takes effect within 60 seconds |
curl -sS -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"protection":{"ip":{"enabled":true,"allow":["203.0.113.0/24"]}}}' \
https://app.wirecanal.com/v1/api/canals/{canal_id}The response is the updated canal representation. protection always contains all 8 sections (secret values are never returned):
"protection": {
"ip": {"enabled": true, "allow": ["203.0.113.0/24"]},
"basic": {"enabled": true},
"bearer": {"enabled": true, "count": 2},
"country": {"enabled": true, "mode": "allow", "list": ["JP"]},
"schedule": {"enabled": true, "tz": "Asia/Tokyo", "rules": [{"days": [1, 2, 3, 4, 5], "from": "09:00", "to": "18:00"}]},
"path": {"enabled": true, "scanner_block": true, "rules": [{"action": "deny", "pattern": "/admin/*"}]},
"fail2ban": {"enabled": true},
"stealth": {"enabled": true}
}Per-Path Extra Conditions (path.rules[].require)
An allow rule in the path restriction can carry extra conditions that apply to that path only (source-IP restriction, BASIC / Bearer authentication). You can keep the canal public overall while requiring an internal IP plus BASIC auth just for everything under /admin — built entirely through the API.
curl -sS -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"protection":{"path":{"enabled":true,"rules":[
{"action":"allow","pattern":"/admin/*","require":{"ip":["203.0.113.0/24"],"basic":{"user":"ops","pass":"<password>"}}},
{"action":"allow","pattern":"/*"}
]}}}' \
https://app.wirecanal.com/v1/api/canals/{canal_id}require.ip… Restrict the source of connections to that path by IP / CIDR (ANDed with the canal-wide IP restriction). Non-matching requests get 403.require.basic/require.bearer… Require authentication for that path only. On a path with its own auth condition, that auth is the only key — the canal-wide credentials will not get you through.- The same element (basic with basic, bearer with bearer) cannot be configured both canal-wide and per-path at the same time (400
bad_path). - Responses never return the credentials inside
require(basiccomes back as{"enabled": true},beareras{"enabled": true, "count": n}, andipas its actual values).
Blocking Common Scanner Paths in One Go (path.scanner_block)
With "scanner_block": true, the paths that automated internet scans routinely probe (/wp-admin, /.env, /.git, /phpmyadmin, and so on — about 30 patterns) are always blocked with 404. This check runs before any other protection and gives away no information about what exists. If auto-blocking (fail2ban) is enabled, hits on these paths count toward it as well. You can also leave rules empty and use scanner_block alone.
If even one section inside protection is invalid, none of that PATCH's protection changes are saved (400; settings unchanged). Only the sections you write are replaced; the sections you do not write remain as they were.
Delete a Canal
Deletes the canal (its access logs are deleted along with it). The public URL stops working immediately after deletion.
curl -sS -X DELETE -H "Authorization: Bearer $KEY" \
https://app.wirecanal.com/v1/api/canals/{canal_id}{"success": true}Pause Publication
Pauses publication (the public side starts returning 404). The response is the updated canal representation with user_paused set to true.
curl -sS -X POST -H "Authorization: Bearer $KEY" \
https://app.wirecanal.com/v1/api/canals/{canal_id}/pause{
"canal_id": "...",
"user_paused": true,
"...": "the other fields have the same shape as the canal representation (Canal details)"
}Resume Publication
Lifts the pause. The response is the canal representation with user_paused set to false. A suspension imposed by the operator cannot be lifted this way (403 suspended_by_operator).
curl -sS -X POST -H "Authorization: Bearer $KEY" \
https://app.wirecanal.com/v1/api/canals/{canal_id}/resume{
"canal_id": "...",
"user_paused": false,
"...": "the other fields have the same shape as the canal representation (Canal details)"
}Fetch wirecanal.json
Returns the Agent configuration for one canal (the contents of wirecanal.json). You can save it directly as wirecanal.json.
curl -sS -H "Authorization: Bearer $KEY" \
https://app.wirecanal.com/v1/api/canals/{canal_id}/agent-json{"access_key": "ck_...", "forward_target": "localhost:8080"}For an MCP canal, the response has 4 keys, adding mode and tools ({"default":"deny","allow":[]}).
Fetch Multiple Canals at Once
Returns the multi-canal format ({"canals":[...]}) for running several canals together on one machine (1–32 entries; if even one is unknown or belongs to someone else, the whole request returns 404).
curl -sS -H "Authorization: Bearer $KEY" \
"https://app.wirecanal.com/v1/api/agent-json?ids=<id1>,<id2>"{
"canals": [
{"access_key": "ck_...", "forward_target": "localhost:8080"},
{"access_key": "ck_...", "forward_target": "localhost:3000"}
]
}Record a Grouped-Operation Set
Records a grouped-operation set (2 or more entries = form a group; 1 entry = dissolve it).
| Parameter | Type | Description | |
|---|---|---|---|
| ids | Required | string[] | Array of canal_id (1–32 entries; 2 or more forms a group, 1 dissolves it) |
curl -sS -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"ids":["<id1>","<id2>"]}' \
https://app.wirecanal.com/v1/api/canals/agent-group{"success": true, "group": "..."}Fetch the Access Log
Returns the accesses that reached the canal, newest first, 100 per page. One row = {ts, ip, method, path, result, ua, referer}. result is the forwarding destination's response code or the block reason.
| Parameter | Type | Description | |
|---|---|---|---|
| offset | Optional | integer | Start position (default 0; 100 entries per page) |
curl -sS -H "Authorization: Bearer $KEY" \
"https://app.wirecanal.com/v1/api/canals/{canal_id}/access-log?offset=0"{
"entries": [
{"ts": 1720000000000, "ip": "203.0.113.5", "method": "GET", "path": "/", "result": 200, "ua": "...", "referer": "..."}
],
"total": 1,
"offset": 0,
"pageSize": 100
}Fetch the Event Log
Returns the administrative events on the canal (creation, forwarding-destination changes, protection changes, pause/resume, deletion, automatic deletion on expiry, certificate issuance/renewal failures, and so on), newest first, 100 per page. Visitor accesses go to the access log; this is the ledger of "who did what, when, and from which IP" (retained 365 days).
| Parameter | Type | Description | |
|---|---|---|---|
| offset | Optional | integer | Start position (default 0; 100 entries per page) |
| category | Optional | string | Filter by operation (operations) / system (automatic processing) / limit (limits) / error (errors) |
curl -sS -H "Authorization: Bearer $KEY" "https://app.wirecanal.com/v1/api/canals/{canal_id}/events?category=operation"{
"entries": [
{
"event_id": 123,
"ts": 1720000000000,
"category": "operation",
"event_type": "canal.forward_target_updated",
"canal_id": "ab12",
"hostname": "ab12xyz0.ja001.wirecanal.com",
"actor": "api",
"actor_uid": "…",
"api_key_id": "ak_…",
"ip": "203.0.113.5",
"detail": {"from": "localhost:3000", "to": "localhost:8080"}
}
],
"total": 1,
"offset": 0,
"pageSize": 100
}actor is user (dashboard operation) / api (in which case api_key_id even identifies which API key performed the operation) / operator (the service operator) / system (automatic processing). ip is the actual source of the operation (not returned for operator actions and automatic processing). After a canal is deleted, its history remains available in the dashboard's "Event history".
Request a Transfer
Sends a request to move ownership of a canal to another WireCanal user (a two-step flow — request → the recipient's acceptance; both sides need Pro or above). The public hostname, access protection, access logs, and custom domain move together with the canal; the moment the transfer completes, the access_key is reissued and the requester's wirecanal.json becomes invalid. API keys do not move.
| Parameter | Type | Description | |
|---|---|---|---|
| to_email | Required | string | Recipient email address. Whether the recipient is a WireCanal user is revealed neither by the response nor by how long it takes |
curl -sS -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"to_email": "teammate@example.com"}' "https://app.wirecanal.com/v1/api/canals/{canal_id}/transfer"{
"transfer": {
"status": "pending",
"request_id": "tr_1a2b3c4d",
"to_email": "teammate@example.com",
"created_at": 1720000000000,
"expires_at": 1720604800000
}
}Only canals without an expiry can be transferred (canals with an expiry get 400 not_transferable). A request expires automatically after 7 days, and each canal can have only one pending request at a time (duplicates get 409 already_requested). You can have at most 10 requests pending at once, and at most 1 request per 24 hours to the same recipient (exceeding these gets 429 rate_limited). The recipient receives a notification email (it does not include the requester's name or email address). A pending request appears on the canal representation as the transfer field (null when there is none).
Cancel a Request
Cancels a pending transfer request. It also disappears from the recipient's acceptance list (no notification is sent to the recipient). If there is no pending request, the response is 404.
{"success": true}List Transfers
Returns received (pending requests addressed to you — matched against the email address of the API key's owner; no information about the requester is included) and sent (the history of requests you have made).
{
"received": [
{
"request_id": "tr_9z8y7x6w",
"hostname": "ab12xyz0.ja001.wirecanal.com",
"tcp_endpoint": null,
"type": "http",
"has_custom_domain": false,
"created_at": 1720000000000,
"expires_at": 1720604800000
}
],
"sent": [
{
"request_id": "tr_1a2b3c4d",
"canal_id": "ab12",
"hostname": "cd34abcd.ja001.wirecanal.com",
"type": "http",
"to_email": "teammate@example.com",
"to_email_masked": false,
"status": "accepted",
"created_at": 1719000000000,
"expires_at": 1719604800000,
"accepted_at": 1719100000000
}
]
}The recipient address (to_email) in sent is masked to ***@masked 30 days after the request reaches a terminal state (accepted, cancelled, or expired).
Accept a Transfer
Accepts a transfer request addressed to you. The only authorization rule is "the email address of the API key's owner = the request's recipient" (requests not addressed to you, expired requests, and cancelled requests all return the same 404 response). The accepting side also needs Pro or above and a free canal slot; TCP/MCP canals require a plan that can create that type, and canals with a custom domain require a custom-domain-capable plan. If even one condition is unmet, nothing changes (fail-closed).
curl -sS -X POST -H "Authorization: Bearer $KEY" "https://app.wirecanal.com/v1/api/transfers/tr_9z8y7x6w/accept"On completion, the response includes the canal representation plus agent_json (the contents of the new wirecanal.json). The requester's previous wirecanal.json becomes invalid immediately, and any connected tunnels are disconnected automatically. Event logs from before the transfer are not carried over to the new owner.
{
"canal_id": "ab12",
"hostname": "ab12xyz0.ja001.wirecanal.com",
"transfer": null,
"agent_json": {
"access_key": "ck_(newly issued key)",
"forward_target": "localhost:3000"
}
}List Access Keys
The list of "access keys" that protect a canal's public endpoint with a fixed key. For MCP clients that do not support OAuth (Claude Code, Gemini CLI, and other "other AI" clients), you can issue per-user or per-department keys that are attached to every request, and revoke them individually. These are separate from the API operation key (wk_), and the key values themselves are not included in the list (each is shown only once, at issuance).
curl -sS -H "Authorization: Bearer $KEY" "https://app.wirecanal.com/v1/api/canals/{canal_id}/auth-keys"{
"keyauth_enabled": true,
"keys": [
{
"key_id": "mk_1a2b3c4d",
"label": "eigyo-team",
"header_name": "",
"expires_at": 1728000000000,
"revoked_at": null,
"created_at": 1719000000000,
"last_used_at": 1719100000000
}
]
}keyauth_enabled indicates whether access-key authentication itself is on or off (issuing a key turns it on automatically). A key whose header_name is empty is matched via Authorization: Bearer <key>; a key with a name is matched via that header (e.g. X-API-Key).
Issue an Access Key
Issues an access key. The key value (plaintext starting with mk_) is included in this response once and can never be retrieved again. For services whose source IPs cannot be pinned down, we recommend setting an expiry and rotating keys regularly. Up to 20 active keys per canal.
| Parameter | Type | Description | |
|---|---|---|---|
| label | Required | string | A name such as the user or department (1–64 characters) |
| header | Optional | string | Header name used for matching. When omitted, the Authorization: Bearer form is used |
| expires_in_days | Optional | number | Validity in days (1–3650). When omitted, the key never expires |
curl -sS -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"label": "eigyo-team", "expires_in_days": 90}' "https://app.wirecanal.com/v1/api/canals/{canal_id}/auth-keys"{
"key_id": "mk_1a2b3c4d",
"label": "eigyo-team",
"header": "authorization",
"expires_at": 1728000000000,
"plaintext": "mk_(the key value — only in this response)"
}Once issued, access to the public side is authenticated with this key (mismatched or missing keys get 401). Which key was used is recorded in the access log as key_id (the key value itself is never recorded).
Revoke an Access Key
Revokes a key individually (this cannot be undone). Revocation takes effect immediately and propagates to the edges within 60 seconds. Nonexistent keys, other users' keys, and already-revoked keys all return the same 404 response.
{"success": true}List Supported Services (MCP Canals)
The status list of an MCP canal's "supported services" (which AI services it can be used from). Enabling one automatically adds the authentication configuration that AI needs (such as an OAuth client) to the canal. It is the same mechanism as the dashboard's "MCP connections" tab. auth is that service's authentication method (oauth / none). Non-MCP canals get 400 bad_type.
{
"connectors": [
{"name": "claude", "label": "Claude", "auth": "oauth", "available": true, "enabled": true, "client_id": "oc_25f053ba349f"},
{"name": "chatgpt", "label": "ChatGPT", "auth": "oauth", "available": true, "enabled": false, "client_id": null},
{"name": "grok", "label": "Grok", "auth": "oauth", "available": true, "enabled": false, "client_id": null},
{"name": "bestllam", "label": "Bestllam", "auth": "oauth", "available": true, "enabled": false, "client_id": null},
{"name": "public", "label": "Public (test)", "auth": "none", "available": true, "enabled": false}
]
}Enable a Supported Service
name is bestllam / claude / chatgpt / grok / public. Enabling is idempotent (calling it again does not add more configuration).
- Bestllam / Claude / ChatGPT / Grok: automatically issues an OAuth client for the connection. The
client_secretin the response is shown this one time only. Paste it into the AI service's connector settings (advanced OAuth settings); when connecting, WireCanal's consent screen opens, and the connection is established once the canal owner approves - public: records a time-limited (7-day) unauthenticated test publication. When the period ends, it terminates automatically and authentication becomes required again
curl -sS -X PUT -H "Authorization: Bearer $KEY" "https://app.wirecanal.com/v1/api/canals/{canal_id}/connectors/claude"{
"name": "claude",
"enabled": true,
"client_id": "oc_25f053ba349f",
"client_secret": "os_(client secret — only in this response)"
}Disable a Supported Service
For Bestllam / Claude / ChatGPT / Grok (name = bestllam / claude / chatgpt / grok), the connection client is revoked and any issued access stops as well. For public, the test-publication record is removed. If it was already disabled, changed: false is returned (idempotent).
{"success": true, "changed": true}List Org IdP Settings (MCP Connection Approval with Company Accounts)
The list of IdP settings for delegating the login on an MCP canal's connection-approval screen to your company's identity provider (OIDC; e.g. Google Workspace). Assign one to a canal, and organization members on the allowed domains can log in with their company accounts to approve connections (members do not need WireCanal accounts; approval via the canal owner's own login keeps working as before). When an account is disabled on the IdP side, that member's connections stop automatically as well (access is re-checked at the hourly refresh). Client secrets are never returned (only has_secret). Available on the Lite plan and above (adding settings or assigning them on a plan without the feature returns 403 plan_required). For a setup guide with screenshots, see the guide to letting your organization's members use it.
{
"idps": [
{"idp_id": "oi_1a2b3c4d5e6f", "label": "Company Google Workspace", "issuer_url": "https://accounts.google.com",
"client_id": "xxxx.apps.googleusercontent.com", "has_secret": true, "allowed_domains": ["example.co.jp"]}
]
}Add an Org IdP Setting
Register WireCanal as a client with your company's identity provider (OIDC-compliant), then store those values here. On the IdP side, register https://app.wirecanal.com/oauth/idp/callback as the redirect URI. Before saving, the existence of issuer_url is verified (OIDC discovery); if it cannot be reached, the response is 400 bad_issuer.
| Parameter | Type | Description | |
|---|---|---|---|
| issuer_url | Required | string | OIDC issuer (e.g. https://accounts.google.com; https only) |
| client_id | Required | string | The client ID registered with the IdP |
| client_secret | Optional | string | Client secret (never shown again after saving) |
| allowed_domains | Required | array / string | Email domains allowed to approve connections (exact match; at least one. A comma-separated string is also accepted) |
| label | Optional | string | Display name |
curl -sS -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"label": "Company Google Workspace", "issuer_url": "https://accounts.google.com", "client_id": "xxxx.apps.googleusercontent.com", "client_secret": "…", "allowed_domains": ["example.co.jp"]}' "https://app.wirecanal.com/v1/api/oauth-idps"Delete an Org IdP Setting
Deletes the setting. All connection approvals issued through this IdP are revoked, and it is automatically detached from any canals it was assigned to.
{"success": true}Assign an IdP to a Canal / Status / Detach
Assigns an IdP to an MCP canal (body is {"idp_id": "oi_…"}; idempotent). Non-MCP canals get 400 bad_type; an IdP not in your own settings gets 404 idp_not_found. Reassigning to a different IdP revokes the approvals issued through the previous one.
Returns the assignment status ({"idp_id": null} when unassigned).
Detaches the assignment. Connection approvals issued through this IdP are revoked (the owner's own approval is unaffected). If nothing was assigned, changed: false is returned (idempotent).
curl -sS -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"idp_id": "oi_1a2b3c4d5e6f"}' "https://app.wirecanal.com/v1/api/canals/{canal_id}/idp"Tool-Permission Proposals (Get / Save / Withdraw)
Saves a proposal for which tools to show to AI (MCP canals only). Here is the important part: only the proposal is saved — the actual allowlist (the on-premises wirecanal.json) changes only when someone runs wirecanal apply-policy on the machine running the Agent and approves it (separation of proposal and approval). allow is saved after whitespace trimming, de-duplication, and sorting (1–128 characters each; up to 256 entries).
| Parameter | Type | Description | |
|---|---|---|---|
| default | Optional | string | deny (allow only the selected tools; default) or allow (allow everything as a rule) |
| allow | Optional | string[] | List of tool names to allow |
| deny_destructive | Optional | boolean | Reject tools with destructive names (delete/drop, etc.) even if they are on the allowlist |
| hide_denied_in_list | Optional | boolean | Also omit denied tools from the tool-list response |
curl -sS -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"default": "deny", "allow": ["get_sales_summary", "search_products"], "deny_destructive": true}' "https://app.wirecanal.com/v1/api/canals/{canal_id}/tool-policy"Returns the proposal and its fingerprints (no proposal returns policy: null). desired_fp is the proposal's fingerprint, and last_tools_fp is the fingerprint of the effective configuration last reported by the Agent (v0.17 and above); if the two match, the proposal has been applied (the same signal the dashboard uses for its "applied / unapproved changes" indicator).
{
"policy": {
"allow": ["get_sales_summary", "search_products"],
"default": "deny",
"deny_destructive": true,
"hide_denied_in_list": false
},
"desired_fp": "f01fc5b0ded997fc",
"last_tools_fp": "f01fc5b0ded997fc"
}Withdraws the proposal. Settings already applied on the Agent side are unaffected. If there was no proposal to begin with, changed: false is returned (idempotent).
Format and limit validation errors return 400 bad_tool_policy; non-MCP canals return 400 bad_type.
Attach a Custom Domain
Attaches a domain you own (e.g. app.example.co.jp) to a canal (custom-domain-capable plans; HTTP/MCP canals only). On registration, the DNS records you need to configure are returned as custom_domain.required_dns_records. It is the same mechanism as the dashboard's connection journey, so you can even start via the API and finish in the UI.
| Parameter | Type | Description | |
|---|---|---|---|
| domain | Required | string | The domain to attach (subdomain, apex, or wildcard *.example.co.jp form) |
curl -sS -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"domain": "app.example.co.jp"}' "https://app.wirecanal.com/v1/api/canals/{canal_id}/custom-domain"{
"custom_domain": {
"domain": "app.example.co.jp",
"status": "dns_pending",
"required_dns_records": [
{"method": "cname", "type": "CNAME", "name": "app.example.co.jp", "value": "ab12xyz0.ja001.wirecanal.com"},
{"method": "a_txt", "type": "A", "name": "app.example.co.jp", "value": "203.0.113.10"},
{"method": "a_txt", "type": "TXT", "name": "_wirecanal-verify.app.example.co.jp", "value": "wcv_..."}
],
"shard_apex": "ja001.wirecanal.com"
}
}For required_dns_records, choose one method and configure it: for a subdomain, the single cname row; for an apex (a bare domain where CNAME cannot be placed), the a_txt A + TXT pair. If the A value could not be obtained, you can resolve the shard_apex name and use the same value. Until the connection completes, the records to configure are also included in the canal-details GET.
POSTing the same domain again is not an error — it returns the current state with 200 (safe to retry). To switch to a different domain, detach and re-register (409 already_attached). A domain in use by another canal returns 409 domain_taken; an insufficient plan returns 403 plan_required_domain.
Wildcards (the *.example.co.jp form) can also be specified. Subdomains underneath (one level) and the bare domain can be attached together with a single certificate, and required_dns_records returns two CNAMEs: ① for traffic (*.example.co.jp → the canal's internal hostname) and ② for certificate issuance (_acme-challenge.example.co.jp → a dedicated name assigned at registration. Set it once, and subsequent automatic certificate renewals keep working over that same record). Once both are configured and the DNS check passes, everything proceeds automatically through certificate issuance. If you have a custom domain attached individually, such as app.example.co.jp, that hostname always takes precedence.
Verify Domain DNS
Verifies the DNS record configuration. Once verification passes, it proceeds all the way to automatic certificate issuance (the same behavior as the dashboard). After that, just poll the canal-details GET until custom_domain.status becomes active (issuance usually takes 1–2 minutes).
{
"verified": true,
"issue": {"kicked": true, "status": "issuing"}
}{
"verified": false,
"observed": {"cname": null, "a": [], "txt": []},
"message": "CNAME が見つかりません。DNS の反映をお待ちください。"
}A failed check changes nothing, and you can call it as many times as you like. observed shows "how the records look right now", so you can compare it against what you configured. Issuance attempts are capped (5 per hour); when exceeded, issue.error = "rate_limited" is returned along with the time you can retry (the verification itself still succeeds — wait a while and verify again). Verifying while issuance is in progress or already live returns 400 bad_state.
Detach a Custom Domain
Detaches the connection (possible from any state). The verification TXT value is discarded; re-registering issues a new one. DELETE on a canal with no domain attached returns 404.
{"success": true}OpenAPI Specification
The machine-readable source of truth is OpenAPI 3.1, published without authentication:
Every endpoint, parameter, and response format described in this reference is available there in machine-readable form.