WireCanalby Qualiteg

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)
workspace_premiumAvailable on the Pro plan and above

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).

http
Authorization: Bearer wk_(your key)
shieldNo cookies or CSRF tokens needed (and none are used)

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.

bash
KEY="wk_(your key)"

Check Your Quota

GET/v1/api/me

Returns your plan, canal limit, usage count, and the name of the API key in use.

bash
curl -sS -H "Authorization: Bearer $KEY" \
     https://app.wirecanal.com/v1/api/me
200 OK
{
  "uid": "...",
  "plan_key": "premium",
  "max_canals": 20,
  "used_canals": 3,
  "api_key_name": "ci-bot"
}

Edge List

GET/v1/api/shards

The list of hosting edges (shards) you can specify when creating a canal.

bash
curl -sS -H "Authorization: Bearer $KEY" \
     https://app.wirecanal.com/v1/api/shards
200 OK
{
  "shards": [
    {"shard_id": "ja000"},
    {"shard_id": "ja001"},
    {"shard_id": "ja100"},
    {"shard_id": "ja200"},
    {"shard_id": "jan000"}
  ]
}

List Canals

GET/v1/api/canals

Returns all canals you own (each element is the same canal representation as in "Canal details").

bash
curl -sS -H "Authorization: Bearer $KEY" \
     https://app.wirecanal.com/v1/api/canals
200 OK
{
  "canals": [ /* array of canal representations (see "Canal details" below) */ ]
}

Create a Canal

POST/v1/api/canals

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.

ParameterTypeDescription
forward_targetOptionalstringForwarding destination (host:port; defaults to localhost:3000)
typeOptionalstringhttp (default) / tcp / mcp
subdomainOptionalstringPreferred subdomain (paid plans)
custom_domainOptionalstringCustom domain (Premium)
shardOptionalstringHosting edge (from the candidates in GET /v1/api/shards)
memoOptionalstringUsage memo (up to 2000 characters; also shown in the dashboard's list and detail views)
param_a / param_b / param_cOptionalstringExtra parameters (up to 512 characters each). An API-only place to store arbitrary attributes, like tags (not shown in the UI)
bash
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
201 Created
{
  "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"}
}
boltWired up in one round trip

Save the response's agent_json as your wirecanal.json, then just start the Agent — and you are live.

Canal Details

GET/v1/api/canals/{canal_id}

Returns a single canal representation. Secret values (BASIC username/password, actual Bearer tokens) are never returned.

bash
curl -sS -H "Authorization: Bearer $KEY" \
     https://app.wirecanal.com/v1/api/canals/{canal_id}
200 OK
{
  "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

PATCH/v1/api/canals/{canal_id}

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).

ParameterTypeDescription
forward_targetOptionalstringChange the forwarding destination
protectionOptionalobject8 sections (ip / basic / bearer / country / schedule / path / fail2ban / stealth)
memoOptionalstringUpdate the usage memo (up to 2000 characters; null or an empty string clears it, omitting leaves it unchanged)
param_a / param_b / param_cOptionalstringUpdate the extra parameters (up to 512 characters each; null or an empty string clears it, omitting leaves it unchanged)
response_timeout_secOptionalintegerOverride 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
bash
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):

json
"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.

bash
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 (basic comes back as {"enabled": true}, bearer as {"enabled": true, "count": n}, and ip as 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.

ruleValidation is fail-closed

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

DELETE/v1/api/canals/{canal_id}

Deletes the canal (its access logs are deleted along with it). The public URL stops working immediately after deletion.

bash
curl -sS -X DELETE -H "Authorization: Bearer $KEY" \
     https://app.wirecanal.com/v1/api/canals/{canal_id}
200 OK
{"success": true}

Pause Publication

POST/v1/api/canals/{canal_id}/pause

Pauses publication (the public side starts returning 404). The response is the updated canal representation with user_paused set to true.

bash
curl -sS -X POST -H "Authorization: Bearer $KEY" \
     https://app.wirecanal.com/v1/api/canals/{canal_id}/pause
200 OK
{
  "canal_id": "...",
  "user_paused": true,
  "...": "the other fields have the same shape as the canal representation (Canal details)"
}

Resume Publication

POST/v1/api/canals/{canal_id}/resume

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).

bash
curl -sS -X POST -H "Authorization: Bearer $KEY" \
     https://app.wirecanal.com/v1/api/canals/{canal_id}/resume
200 OK
{
  "canal_id": "...",
  "user_paused": false,
  "...": "the other fields have the same shape as the canal representation (Canal details)"
}

Fetch wirecanal.json

GET/v1/api/canals/{canal_id}/agent-json

Returns the Agent configuration for one canal (the contents of wirecanal.json). You can save it directly as wirecanal.json.

bash
curl -sS -H "Authorization: Bearer $KEY" \
     https://app.wirecanal.com/v1/api/canals/{canal_id}/agent-json
200 OK
{"access_key": "ck_...", "forward_target": "localhost:8080"}
smart_toyMCP canals have 4 keys

For an MCP canal, the response has 4 keys, adding mode and tools ({"default":"deny","allow":[]}).

Fetch Multiple Canals at Once

GET/v1/api/agent-json?ids=<id1>,<id2>

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).

bash
curl -sS -H "Authorization: Bearer $KEY" \
     "https://app.wirecanal.com/v1/api/agent-json?ids=<id1>,<id2>"
200 OK
{
  "canals": [
    {"access_key": "ck_...", "forward_target": "localhost:8080"},
    {"access_key": "ck_...", "forward_target": "localhost:3000"}
  ]
}

Record a Grouped-Operation Set

POST/v1/api/canals/agent-group

Records a grouped-operation set (2 or more entries = form a group; 1 entry = dissolve it).

ParameterTypeDescription
idsRequiredstring[]Array of canal_id (1–32 entries; 2 or more forms a group, 1 dissolves it)
bash
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
200 OK
{"success": true, "group": "..."}

Fetch the Access Log

GET/v1/api/canals/{canal_id}/access-log?offset=0

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.

ParameterTypeDescription
offsetOptionalintegerStart position (default 0; 100 entries per page)
bash
curl -sS -H "Authorization: Bearer $KEY" \
     "https://app.wirecanal.com/v1/api/canals/{canal_id}/access-log?offset=0"
200 OK
{
  "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

GET/v1/api/canals/{canal_id}/events?offset=0&category=

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).

ParameterTypeDescription
offsetOptionalintegerStart position (default 0; 100 entries per page)
categoryOptionalstringFilter by operation (operations) / system (automatic processing) / limit (limits) / error (errors)
bash
curl -sS -H "Authorization: Bearer $KEY"      "https://app.wirecanal.com/v1/api/canals/{canal_id}/events?category=operation"
200 OK
{
  "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

POST/v1/api/canals/{canal_id}/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.

ParameterTypeDescription
to_emailRequiredstringRecipient email address. Whether the recipient is a WireCanal user is revealed neither by the response nor by how long it takes
bash
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"
202 Accepted
{
  "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

POST/v1/api/canals/{canal_id}/transfer/cancel

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.

200 OK
{"success": true}

List Transfers

GET/v1/api/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).

200 OK
{
  "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

POST/v1/api/transfers/{request_id}/accept

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).

bash
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.

200 OK (excerpt)
{
  "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

GET/v1/api/canals/{canal_id}/auth-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).

bash
curl -sS -H "Authorization: Bearer $KEY"      "https://app.wirecanal.com/v1/api/canals/{canal_id}/auth-keys"
200 OK
{
  "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

POST/v1/api/canals/{canal_id}/auth-keys

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.

ParameterTypeDescription
labelRequiredstringA name such as the user or department (1–64 characters)
headerOptionalstringHeader name used for matching. When omitted, the Authorization: Bearer form is used
expires_in_daysOptionalnumberValidity in days (1–3650). When omitted, the key never expires
bash
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"
201 Created
{
  "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

DELETE/v1/api/canals/{canal_id}/auth-keys/{key_id}

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.

200 OK
{"success": true}

List Supported Services (MCP Canals)

GET/v1/api/canals/{canal_id}/connectors

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.

200 OK (excerpt)
{
  "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

PUT/v1/api/canals/{canal_id}/connectors/{name}

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_secret in 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
bash
curl -sS -X PUT -H "Authorization: Bearer $KEY"      "https://app.wirecanal.com/v1/api/canals/{canal_id}/connectors/claude"
200 OK (claude; client_secret only in this response)
{
  "name": "claude",
  "enabled": true,
  "client_id": "oc_25f053ba349f",
  "client_secret": "os_(client secret — only in this response)"
}

Disable a Supported Service

DELETE/v1/api/canals/{canal_id}/connectors/{name}

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).

200 OK
{"success": true, "changed": true}

List Org IdP Settings (MCP Connection Approval with Company Accounts)

GET/v1/api/oauth-idps

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.

200 OK
{
  "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

POST/v1/api/oauth-idps

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.

ParameterTypeDescription
issuer_urlRequiredstringOIDC issuer (e.g. https://accounts.google.com; https only)
client_idRequiredstringThe client ID registered with the IdP
client_secretOptionalstringClient secret (never shown again after saving)
allowed_domainsRequiredarray / stringEmail domains allowed to approve connections (exact match; at least one. A comma-separated string is also accepted)
labelOptionalstringDisplay name
bash
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

DELETE/v1/api/oauth-idps/{idp_id}

Deletes the setting. All connection approvals issued through this IdP are revoked, and it is automatically detached from any canals it was assigned to.

200 OK
{"success": true}

Assign an IdP to a Canal / Status / Detach

PUT/v1/api/canals/{canal_id}/idp

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.

GET/v1/api/canals/{canal_id}/idp

Returns the assignment status ({"idp_id": null} when unassigned).

DELETE/v1/api/canals/{canal_id}/idp

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).

bash
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)

PUT/v1/api/canals/{canal_id}/tool-policy

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).

ParameterTypeDescription
defaultOptionalstringdeny (allow only the selected tools; default) or allow (allow everything as a rule)
allowOptionalstring[]List of tool names to allow
deny_destructiveOptionalbooleanReject tools with destructive names (delete/drop, etc.) even if they are on the allowlist
hide_denied_in_listOptionalbooleanAlso omit denied tools from the tool-list response
bash
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"
GET/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).

200 OK
{
  "policy": {
    "allow": ["get_sales_summary", "search_products"],
    "default": "deny",
    "deny_destructive": true,
    "hide_denied_in_list": false
  },
  "desired_fp": "f01fc5b0ded997fc",
  "last_tools_fp": "f01fc5b0ded997fc"
}
DELETE/v1/api/canals/{canal_id}/tool-policy

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

POST/v1/api/canals/{canal_id}/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.

ParameterTypeDescription
domainRequiredstringThe domain to attach (subdomain, apex, or wildcard *.example.co.jp form)
bash
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"
201 Created (custom_domain excerpt)
{
  "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

POST/v1/api/canals/{canal_id}/custom-domain/verify

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).

200 OK (verified)
{
  "verified": true,
  "issue": {"kicked": true, "status": "issuing"}
}
200 OK (not verified yet)
{
  "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

DELETE/v1/api/canals/{canal_id}/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.

200 OK
{"success": true}

OpenAPI Specification

The machine-readable source of truth is OpenAPI 3.1, published without authentication:

GEThttps://app.wirecanal.com/v1/api/openapi.json

Every endpoint, parameter, and response format described in this reference is available there in machine-readable form.