Skip to main content

Gateway — Provider Connections API

Base path: /api/v1/gateway/connections. BYOK (bring-your-own-key) provider credentials, encrypted at rest (AES-256-GCM). The plaintext key is never returned — only keyLastFour.

Auth: session cookie or personal/team API key (requireAnyAuth). Mutations (POST/PATCH/DELETE) require role owner or admin; reads (GET) allow any role. Supported provider values: openai, anthropic, gemini, openai_compatible (the latter requires config.base_url).

All examples below were verified with real curl output against a running server (dev DB, real GATEWAY_ENCRYPTION_KEY). Provider keys are redacted here as sk-...REDACTED.


POST /api/v1/gateway/connections​

Create a provider connection (owner/admin). config defaults to {}.

curl -X POST $ACRUXCORE_BASE_URL/gateway/connections \
-H "Content-Type: application/json" \
--cookie "connect.sid=<session>" \
-d '{"provider":"openai","label":"OpenAI Prod","apiKey":"sk-...REDACTED"}'

Response (status 201):

{
"id": "73c7783a-78ca-40a0-b79c-4e6026959fc8",
"provider": "openai",
"label": "OpenAI Prod",
"keyLastFour": "E3AA",
"config": {},
"createdBy": "6353f9ff-dc41-4b9f-9a83-237002cfc8e7",
"createdAt": "2026-07-04T06:41:36.413Z",
"updatedAt": "2026-07-04T06:41:36.413Z"
}

A gemini connection is created the same way (used below to route Gemini completions):

curl -X POST $ACRUXCORE_BASE_URL/gateway/connections \
-H "Content-Type: application/json" \
--cookie "connect.sid=<session>" \
-d '{"provider":"gemini","label":"Gemini Prod","apiKey":"sk-...REDACTED"}'

Response (status 201):

{
"id": "ce9c1c55-1f36-47ee-bbe2-56648cfde2b8",
"provider": "gemini",
"label": "Gemini Prod",
"keyLastFour": "jZMg",
"config": {},
"createdBy": "6353f9ff-dc41-4b9f-9a83-237002cfc8e7",
"createdAt": "2026-07-04T06:41:36.431Z",
"updatedAt": "2026-07-04T06:41:36.431Z"
}

For openai_compatible, config.base_url is required and must be a valid URL (omitting it returns 400):

curl -X POST $ACRUXCORE_BASE_URL/gateway/connections \
-H "Content-Type: application/json" \
--cookie "connect.sid=<session>" \
-d '{"provider":"openai_compatible","label":"Local vLLM","apiKey":"sk-...REDACTED","config":{"base_url":"https://api.example.com/v1"}}'

Response (status 201):

{
"id": "e5b6499c-57af-4abc-bc5e-6562b33037cc",
"provider": "openai_compatible",
"label": "Local vLLM",
"keyLastFour": "-xyz",
"config": { "base_url": "https://api.example.com/v1" },
"createdBy": "6353f9ff-dc41-4b9f-9a83-237002cfc8e7",
"createdAt": "2026-07-04T06:41:36.443Z",
"updatedAt": "2026-07-04T06:41:36.443Z"
}

Some OpenAI-compatible providers refuse a call without a header of their own. Put it in config.headers and the gateway sends it on every call to that connection. Values are stored in plain text and shown to anyone who can list connections, so never put a key there: the key goes in apiKey. Only openai_compatible connections accept config.headers, at most 20 of them. Headers that carry auth or that the gateway sets itself (Authorization, Host, Content-Type, Cookie, x-api-key and similar) are refused with 400. So is any name that looks like it carries a credential, such as x-goog-api-key or x-auth-token, and two names that differ only in upper and lower case.

curl -X POST $ACRUXCORE_BASE_URL/gateway/connections \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"provider":"openai_compatible","label":"Gemini (OpenAI-compatible)","apiKey":"AIza...REDACTED","config":{"base_url":"https://generativelanguage.googleapis.com/v1beta/openai","headers":{"x-opencode-session":"acx-gateway"}}}'

Response (status 201):

{
"id": "fea63d88-1bc2-4d85-bd49-64e853e1abc1",
"provider": "openai_compatible",
"label": "Gemini (OpenAI-compatible)",
"keyLastFour": "_sGg",
"config": {
"headers": { "x-opencode-session": "acx-gateway" },
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai"
},
"createdBy": "e2795a8f-d3db-4704-8bc4-d8e24a971578",
"createdAt": "2026-11-06T20:34:44.097Z",
"updatedAt": "2026-11-06T20:34:44.097Z"
}

A refused header:

curl -X POST $ACRUXCORE_BASE_URL/gateway/connections \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"provider":"openai_compatible","label":"x","apiKey":"sk-x","config":{"base_url":"https://api.example.com/v1","headers":{"Authorization":"Bearer other"}}}'

Response (status 400):

{"error":{"code":"VALIDATION_ERROR","message":"This header cannot be set on a connection: auth headers belong in apiKey, and the gateway sets the rest itself."}}

A name that looks like it carries a credential is refused the same way:

curl -X POST $ACRUXCORE_BASE_URL/gateway/connections \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"provider":"openai_compatible","label":"x","apiKey":"sk-x","config":{"base_url":"https://api.example.com/v1","headers":{"x-goog-api-key":"AIza-example"}}}'

Response (status 400):

{"error":{"code":"VALIDATION_ERROR","message":"This header looks like it carries a credential. Connection headers are readable by every team member, so put the key in apiKey instead."}}

To remove the headers, PATCH {"config":{"headers":null}}. A PATCH that sends headers replaces the whole map.


GET /api/v1/gateway/connections​

List all connections for the team (masked), newest first.

curl $ACRUXCORE_BASE_URL/gateway/connections \
--cookie "connect.sid=<session>"

Response (status 200):

[
{
"id": "e5b6499c-57af-4abc-bc5e-6562b33037cc",
"provider": "openai_compatible",
"label": "Local vLLM",
"keyLastFour": "-xyz",
"config": { "base_url": "https://api.example.com/v1" },
"createdBy": "6353f9ff-dc41-4b9f-9a83-237002cfc8e7",
"createdAt": "2026-07-04T06:41:36.443Z",
"updatedAt": "2026-07-04T06:41:36.443Z"
},
{
"id": "ce9c1c55-1f36-47ee-bbe2-56648cfde2b8",
"provider": "gemini",
"label": "Gemini Prod",
"keyLastFour": "jZMg",
"config": {},
"createdBy": "6353f9ff-dc41-4b9f-9a83-237002cfc8e7",
"createdAt": "2026-07-04T06:41:36.431Z",
"updatedAt": "2026-07-04T06:41:36.431Z"
},
{
"id": "73c7783a-78ca-40a0-b79c-4e6026959fc8",
"provider": "openai",
"label": "OpenAI Prod",
"keyLastFour": "E3AA",
"config": {},
"createdBy": "6353f9ff-dc41-4b9f-9a83-237002cfc8e7",
"createdAt": "2026-07-04T06:41:36.413Z",
"updatedAt": "2026-07-04T06:41:36.413Z"
}
]

GET /api/v1/gateway/connections/:id​

Get one masked connection. Returns 404 if it does not belong to the team.

curl $ACRUXCORE_BASE_URL/gateway/connections/73c7783a-78ca-40a0-b79c-4e6026959fc8 \
--cookie "connect.sid=<session>"

Response (status 200):

{
"id": "73c7783a-78ca-40a0-b79c-4e6026959fc8",
"provider": "openai",
"label": "OpenAI Prod",
"keyLastFour": "E3AA",
"config": {},
"createdBy": "6353f9ff-dc41-4b9f-9a83-237002cfc8e7",
"createdAt": "2026-07-04T06:41:36.413Z",
"updatedAt": "2026-07-04T06:41:36.413Z"
}

PATCH /api/v1/gateway/connections/:id​

Update the label and/or config, or rotate the key by supplying a new apiKey (owner/admin). provider is immutable. Below renames a connection.

curl -X PATCH $ACRUXCORE_BASE_URL/gateway/connections/e5b6499c-57af-4abc-bc5e-6562b33037cc \
-H "Content-Type: application/json" \
--cookie "connect.sid=<session>" \
-d '{"label":"Local vLLM (renamed)"}'

Response (status 200):

{
"id": "e5b6499c-57af-4abc-bc5e-6562b33037cc",
"provider": "openai_compatible",
"label": "Local vLLM (renamed)",
"keyLastFour": "-xyz",
"config": { "base_url": "https://api.example.com/v1" },
"createdBy": "6353f9ff-dc41-4b9f-9a83-237002cfc8e7",
"createdAt": "2026-07-04T06:41:36.443Z",
"updatedAt": "2026-07-04T06:41:46.047Z"
}

DELETE /api/v1/gateway/connections/:id​

Delete a connection (owner/admin). Returns 204 with no body.

curl -X DELETE $ACRUXCORE_BASE_URL/gateway/connections/e5b6499c-57af-4abc-bc5e-6562b33037cc \
--cookie "connect.sid=<session>"

Response (status 204) — no body.