HTTP API connector
Any HTTPS API: a base URL, an auth style and a key. Methods and paths stay fenced.
- Status: Available
- Shelf: Developer
- How you connect: API key
- Regions: global (not available to Kazakhstan-only workspaces)
- The service’s own API reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication
Connect any HTTPS API without waiting for a native connector: a base URL on a public host, how the key is sent, the methods and path prefix apps may use, and optional fixed headers. Apps call it through the gate; the key never reaches app code, the host is pinned, and every call is audited.
Connect one (admins)#
/connectors → Add an HTTP API (or Set up on a service's card for a form filled in from its preset, which only asks for a name, the key and, where the service has one, your own address):
| Field | Rule |
|---|---|
| Name | How it appears on the gate in the City |
| Base URL | https://<public host>[/path]. No IP literals, no localhost/.local/.internal/.test names, no userinfo, no query, port 443 only, host ≤ 100 characters. The API also refuses a name that resolves to a private address |
| How the key is sent | Bearer token (Authorization: Bearer …), Custom header (X-Api-Key: …, or a raw Authorization), User name and password (Authorization: Basic …), Secret in the URL path (webhook-style: see below), No key (public APIs: the fence and the audit still apply). There is no query-parameter style: why |
| Key | Write-only, ≥ 8 characters (basic passwords excepted), printable ASCII for header styles; for a path token, plain URL segments. Sealed to this connection and this host: a new host, auth style or user name needs it typed again |
| Methods | Any of GET, POST, PUT, PATCH, DELETE. Read-only? Keep GET alone |
| Path prefix | Apps may only call paths under it (/items allows /items and /items/3, never /itemsx). / allows everything |
| Test path | A harmless GET for Test connection (defaults to the prefix). A connection that can't GET, or that says testable: false (a service that only sends), is not probed: Test connection only opens the sealed key and says nothing was sent |
| Fixed headers | Up to 8 non-secret headers the gate adds (Notion-Version, Ozon's Client-Id). Shown to admins: never a key |
A connection made from a service's preset also carries preset (the catalog entry's id) so the
gallery shows it on that service's card; the HTTP API connector's own card lists every connection.
After saving, the dialog shows the connection id (conn_…), the SDK call and the manifest entry,
and Test connection makes one real GET through the gate ("Works. api.example.com answered 200
in 142 ms", or what failed and the next step). A refused key (401/403) marks the gate Expired.
API: POST /v1/workspaces/:ws/connectors/http with HttpConnectorInput, PUT /v1/workspaces/:ws/connections/:id/http with HttpConnectorUpdate (schemas in
packages/contracts/src/connector-catalog.ts).
Use it from an app#
The manifest names the connection in scope (the builder writes this; a person approves it):
{ "id": "http", "actions": ["request"], "scope": "conn_k3v9…" }const api = ctx.connectors.http('conn_k3v9…')
const low = await api.get<{ items: Item[] }>('/items', { query: { below: 5 } })
await api.post('/items/BEAN-1/reorder', { json: { qty: 20 } })
// put, patch, delete too. A reply outside 2xx throws a ConnectorError (code `upstream_error`,
// with the API's own message); pass { throwOnError: false } to read it yourself.HttpResponse is { status, ok, headers, body, text }; body is parsed JSON when the reply is
JSON. Refusals the gate writes itself carry x-qaldy-gate: refused, so the SDK maps them to
egress_blocked (with reason), not_connected, policy_unavailable… instead of an upstream error.
Path token#
Some services make the URL the credential: a Slack incoming webhook
(https://hooks.slack.com/services/T000/B000/XXXX), a Bitrix24 inbound webhook
(https://<portal>.bitrix24.<tld>/rest/<user>/<code>/<method>). For those, pick Secret in the URL
path (path_token). The base URL holds the part that is not secret (https://hooks.slack.com/services,
https://acme.bitrix24.kz/rest); the secret (T000/B000/XXXX, 1/abcdef…) is sealed, and the gate
builds every upstream path as
<base path> / <secret> / <the app's path>- The secret is one or more
/-separated segments of letters, digits and- _ . ~, none empty and none.or.., at most 512 characters (isHttpPathTokenin contracts,is_path_tokenin the gate). It can add neither a query nor a fragment nor a dot segment. - It always comes before anything the app chose, and the app's path is normalised first
(
..,%2e,%2f,\refused), so an app can't replace it, shorten it or climb out of it. A path that repeats the secret is just more path after the real one. - The reply is scrubbed of the whole secret, of each segment of 6+ characters, and of their percent-encoded forms, in the body and every header (also across chunk boundaries).
- Nothing records the path: audit rows, City events and logs carry the host and the status only.
- In the console a webhook preset takes the whole URL and reads the host (if it is the admin's
own) and the secret part from it (
parsePresetWebhookUrl).
Rust tests: http_connector::tests::a_path_token_is_spliced…, …an_app_cannot_override_the_path_token…,
flow_tests::http_flows::path_token_connections_splice_the_secret_and_the_app_cannot_read_or_override_it.
Why there is no query-parameter style#
A key in ?api_key=… is copied into the upstream's access logs, proxies and browser histories, and
into any error message that quotes the URL. The style was removed:
- Zod refuses
{ style: "query" }with the reason ("A key in the URL query ends up in the service's access logs … Use a header, a bearer token or a path token") on create and update. - The gate refuses a policy that still carries it:
503 connector_query_auth_removed, nothing sent. - A connection stored with it before the removal is read, not lost: the API reports it as not
working, Test connection answers
query_auth_removed, the console says why and opens the form on a bearer token. Moving it to another style needs the key typed again (it was sealed for that style).
What the gate does on every call#
https://connector.qaldy.internal/http/<connection>/<path>?<query>, signed by the runtime host:
- The version's policy must grant
{id: "http", actions: ["request"], scope: <connection>}and list the connection (action_not_declaredotherwise). - Re-validates the connection's settings (
connector_misconfiguredif they don't parse). - Method allowed (
method_not_allowed), path normalised and under the prefix (path_not_allowed:./..segments,%2e, encoded/or\, empty segments are refused). - Opens the sealed key with aad
<workspace>|http:<connection>:<host>|<keyId>(connector_not_connected,connector_secret_unavailable). - Resolves the host once; every address must be public (no loopback, private, link-local or
metadata ranges:
private_address) and it connects to exactly those addresses (no DNS rebinding). TLS with SNI = the host; redirects are never followed. - Builds the request: the app's headers minus routing,
authorization, cookies, proxy andx-qaldy-*headers; the fixed headers; the key per auth style (a header, or spliced into the path);Accept-Encoding: identity. - Streams the reply back (≤ 16 MiB, ≤ 60 s) with every echoed form of the key replaced by
[redacted]in headers and body, also when it is split across chunks (raw,:percent-encoded, form-encoded, base64 basic credentials).Set-Cookieand trailers are dropped. A compressed reply is refused (connector_upstream_encoded), because it can't be checked for the key. - Records
connector.called(connection, host, method, status, duration; never paths, queries, bodies or headers) → an audit row and the City's parcel between the app and its gate.
Threat notes#
| Threat | Mitigation |
|---|---|
| App reaches an internal service through the connection | Host fixed by the admin, public addresses only, pinned per call, path prefix enforced after normalisation |
| App exfiltrates the key | It never enters the isolate; echoes are scrubbed; compressed replies refused |
| Key replayed against another host | aad binds the key to the connection id and host: a moved base URL can't open it |
| Compromised control plane repoints a key | Same binding: the API can't send an existing key to a new host without the plaintext |
| Short key scrubbing mangles replies | Keys under 8 characters are refused (basic passwords: the base64 credentials are scrubbed) |
| Residual | The upstream sees the key (by design). Responses up to 16 MiB stream through; size and rate limits per connection are planned |