Action Connections
Manage connector connections operationally — create, test, list, and resolve tenant-scoped encrypted credential bindings that connector steps dispatch through.
A connection is a tenant-scoped, encrypted binding to a third-party provider: the credentials and configuration that let a workflow action step call HubSpot, Salesforce, Stripe, or any of Ductor's 100+ connectors. You manage connections here — create, test, list, update, revoke — and the runtime resolves them into an auth context at dispatch time. This page is the operational slice; the Connectors section covers providers, actions, and execution.
Where it lives
Connections are served by ConnectorService under
/api/v2/connector/connections and persisted to the connector_connection
table with credentials encrypted at rest (XChaCha20-Poly1305 AEAD). The
application service is at application/connector/; encryption at
infrastructure/connector/crypto/.
What a connection binds together
| Concept | Field | What it is |
|---|---|---|
| Provider family | provider_key | Which connector, e.g. hubspot, salesforce. |
| Provider instance | provider_config_key | A configured instance of that provider (distinct from the family). |
| Auth type | auth_type | secret_text, basic, oauth2, two_step, jwt, … |
| Credentials | (write-only) | The secret material — encrypted, never returned. |
| Non-secret config | connection_config | Region, subdomain, instance URL, etc. |
| Environment | environment_id | Which environment the connection belongs to. |
| Label / tags | label, tags | Human identification and filtering. |
The stored Connection object you read back contains everything except the
credentials:
Prop
Type
Credentials are one-way
Credential values are encrypted with per-connection AEAD the moment they're
received and are never returned on any read. The encryption is bound to
tenant_id + provider_key as additional authenticated data, so a ciphertext
from one tenant cannot be decrypted under another — cross-tenant credential
swaps fail loudly. You can replace credentials (via update) but never read them
back.
Create a connection
CreateConnection — POST /api/v2/connector/connections
(connector:write). Supply the provider, a label, the credentials, and any
non-secret config.
curl -s -X POST https://api.ductor.io/api/v2/connector/connections \
-H "Authorization: Bearer $DUCTOR_API_KEY" \
-H "X-Tenant-ID: $DUCTOR_TENANT" \
-H "Content-Type: application/json" \
-d '{
"provider_key": "hubspot",
"provider_config_key": "hubspot-prod",
"environment_id": "env_9a1b",
"auth_type": "secret_text",
"label": "Acme HubSpot (prod)",
"credentials": { "api_key": "pat-na1-...." },
"connection_config": { "region": "na1" },
"tags": { "env": "prod", "team": "sales" }
}'{
"id": "c1a2b3d4-...",
"tenant_id": "9c8b7a6d-...",
"provider_key": "hubspot",
"provider_config_key": "hubspot-prod",
"auth_type": "secret_text",
"label": "Acme HubSpot (prod)",
"status": "active",
"connection_config": { "region": "na1" },
"tags": { "env": "prod", "team": "sales" },
"created_at": "2026-07-11T11:20:00Z"
}On create, Ductor validates the credential payload against the provider's
schema, encrypts it, and immediately runs the provider's auth test. A connection
that fails the test lands in errored status with the reason on
last_test_error.
OAuth2 providers
For OAuth2 providers you don't send raw credentials — you run the authorize flow:
OAuthStart—POST /api/v2/connector/oauth/start(connector:write) — returns anauthorize_url, a pendingconnection_id, and a CSRFstate.- Redirect the user to
authorize_url; they consent at the provider. OAuthCallback—POST /api/v2/connector/oauth/callback(connector:write) — exchange the returnedcode+statefor tokens; the response is the completedConnection.
curl -s -X POST https://api.ductor.io/api/v2/connector/oauth/start \
-H "Authorization: Bearer $DUCTOR_API_KEY" \
-H "X-Tenant-ID: $DUCTOR_TENANT" \
-H "Content-Type: application/json" \
-d '{
"provider_key": "salesforce",
"provider_config_key": "sf-prod",
"environment_id": "env_9a1b",
"label": "Acme Salesforce",
"scopes": ["refresh_token", "api"]
}'To let an end user connect their own account without exposing your admin
API, mint a short-lived Connect Session
(POST /api/v2/connector/connect-sessions). It returns a one-time token and a
connect_link you hand to the user; they complete OAuth against a hosted
connect page that drives OAuthStart/OAuthCallback for you. See
Connectors for the embeddable flow.
Test a connection
TestConnection — POST /api/v2/connector/connections/{id}/test
(connector:write) — re-runs the provider's auth probe on demand and updates
last_tested_at / last_test_error.
curl -s -X POST https://api.ductor.io/api/v2/connector/connections/$CONN_ID/test \
-H "Authorization: Bearer $DUCTOR_API_KEY" -H "X-Tenant-ID: $DUCTOR_TENANT"{ "ok": false, "error_message": "401 from provider", "error_code": "unauthorized",
"error_category": "auth", "retryable": false, "needs_reconnect": true,
"tested_at": "2026-07-11T11:25:00Z" }needs_reconnect: true is the signal to re-run OAuth or replace credentials;
retryable distinguishes a transient failure from a bad credential.
Read, update, delete
# One connection
curl -s https://api.ductor.io/api/v2/connector/connections/$CONN_ID \
-H "Authorization: Bearer $DUCTOR_API_KEY" -H "X-Tenant-ID: $DUCTOR_TENANT"
# List, filter by provider and/or tags
curl -s "https://api.ductor.io/api/v2/connector/connections?provider_key=hubspot&page_size=50" \
-H "Authorization: Bearer $DUCTOR_API_KEY" -H "X-Tenant-ID: $DUCTOR_TENANT"GetConnection and ListConnections require connector:read. List supports
provider_key, environment_id, provider_config_key, and tags filters
(all supplied tags must match) with the standard page_size/page_token
cursor.
UpdateConnection — PATCH /api/v2/connector/connections/{id}
(connector:write) — replaces the fields you send. Supplying credentials
re-encrypts a fresh secret; supplying connection_config replaces the whole map
(an empty map clears it).
curl -s -X PATCH https://api.ductor.io/api/v2/connector/connections/$CONN_ID \
-H "Authorization: Bearer $DUCTOR_API_KEY" \
-H "X-Tenant-ID: $DUCTOR_TENANT" \
-H "Content-Type: application/json" \
-d '{ "credentials": { "api_key": "pat-na1-rotated" } }'DeleteConnection — DELETE /api/v2/connector/connections/{id}
(connector:write) — removes the connection.
Browsing the catalog
Two read-only surfaces let you discover what you can connect to, both
connector:read:
- Providers —
GET /api/v2/connector/providers(and/providers/{key}) — the connector families, their auth types, categories, and capabilities (supports_oauth,supports_refresh, …). - Actions —
GET /api/v2/connector/actions?provider_key=hubspot(and/actions/{key}) — the fully-qualified actions (e.g.hubspot.create_contact) a provider exposes, which are what a workflow action step'sactionfield references.
How the runtime resolves a connection
You never fetch credentials yourself. When a workflow action step dispatches, the runtime resolves the referenced connection into an auth context:
- Load the
connector_connectionrow (from a short-lived cache, with single-flight de-duplication of misses). - Gate on status — only
activeproceeds;pending/expired/revoked/erroredfail with a typed error. - Decrypt the credentials with the tenant + provider AAD.
- For OAuth2, refresh the token if it's within ~60s of expiry, per-connection single-flighted so concurrent steps don't stampede the refresh.
- Build the auth context the connector uses to make the call.
That auth context is ephemeral and per-dispatch; the plaintext secret never leaves this path and is never persisted decrypted.
Encryption key is required in production
Credential encryption is keyed by connector.encryption_key
(DUCTOR_CONNECTOR_ENCRYPTION_KEY, a base64 32-byte key). In production the
process refuses to start without it. Rotating the key is supported through a keyring so old
ciphertexts stay decryptable during the rotation window. See
Configuration.
Where to go next
Routing & Work Experiments
Run durable experiments across routing strategies, workflow steps, and agent definitions with sticky assignments, immutable exposure receipts, and idempotent delayed scores.
Configuration
The runtime-writable config surface — system config snapshots, feature flags, per-tenant overrides, runtime config refs, and the routing pipeline mode — versus static env configuration.