Connections API
List the connections of your company and check their status with the Connections API, with the parameters and responses for each endpoint.
A connection is Loopfour's saved, authorized access to one account in another system, such as one QuickBooks company. Steps use a connection to call that system. A company can have several connections per provider; one of them is the default.
| Scope | Endpoints |
|---|---|
connections:read | List, get, verify |
connections:write | Create, auth URL, update, set default, refresh, delete, API key connection, proxy a request |
Credentials are never returned. Metadata in responses is redacted.
Connection object
| Field | Type | Meaning |
|---|---|---|
id | string (UUID) | Connection ID. Use it as config.connection in a step |
name | string | Display name, up to 255 characters |
provider | string | Provider ID, for example quickbooks |
status | string | pending, active, expired, error, disconnected or abandoned |
externalAccountId | string or null | The account ID in the other system |
errorMessage | string or null | List only. Last error |
isDefault | boolean | Whether steps use it when they do not name a connection |
authVariant | string or null | The sign-in method, for providers that offer more than one |
lastSyncedAt | string or null | |
metadata | object | Get and update only. Redacted |
createdAt, updatedAt | string (ISO 8601) |
Provider IDs: stripe, salesforce, hubspot, quickbooks, netsuite, pandadoc, slack, xero, gmail, outlook, rillet, attio, sage-intacct, zoho-inventory, docusign, dropbox-sign, airwallex, odoo, avalara, plaid, google-sheets, box and zuora.
List connections
GET /api/v1/connections| Query parameter | Type | Default | Meaning |
|---|---|---|---|
provider | string | A provider ID | |
status | string | A status. Without it, abandoned attempts are left out | |
limit | integer | 20 | 1 to 100 |
cursor | string | meta.cursor from the previous page. An invalid cursor returns 400 |
Defaults come first, then oldest first.
curl 'https://workflow.loopfour.ai/api/v1/connections?provider=quickbooks' \
-H 'x-api-key: wfk_live_your_api_key'{
"success": true,
"data": [
{
"id": "8f14e45f-ceea-4e7a-9b1d-3c2a5d6e7f80",
"name": "QuickBooks - US entity",
"provider": "quickbooks",
"status": "active",
"externalAccountId": "9130354517268936",
"errorMessage": null,
"isDefault": true,
"authVariant": null,
"lastSyncedAt": "2026-02-12T08:00:00.000Z",
"createdAt": "2026-01-05T16:20:00.000Z",
"updatedAt": "2026-02-12T08:00:00.000Z"
}
],
"meta": { "hasMore": false }
}Get a connection
GET /api/v1/connections/{id}Returns the connection with its redacted metadata.
Connect an OAuth provider
Most providers connect by signing in to the other system. Two endpoints start that flow. Both create a pending connection and return an authUrl; send the person who owns the account to it. The connection becomes active when they finish.
POST /api/v1/connections| Field | Type | Required | Meaning |
|---|---|---|---|
name | string | Yes | 1 to 255 characters |
provider | string | Yes | A provider ID |
isDefault | boolean | No | Make it the default for the provider. The first connection of a provider becomes the default |
metadata | object | No | |
connectionConfig | object | No | Provider-specific settings, as strings |
The response is 201 with id, name, provider, status, isDefault, authUrl and message.
GET /api/v1/connections/{provider}/auth-url| Query parameter | Type | Meaning |
|---|---|---|
redirectUrl | string (URL) | Where to send the person afterwards |
connectionName | string | Display name. Defaults to a name based on the provider |
connectionConfig | string | Provider-specific settings as JSON. Invalid JSON returns 400 |
The response has connectionId, provider, authUrl, isDefault and message.
Plaid connects through its own bank-link flow, so these endpoints return 400 for plaid. Rillet, Airwallex, Odoo and Avalara connect with an API key instead.
Connect with an API key
POST /api/v1/connections/{provider}/api-keyprovider is rillet, airwallex, odoo or avalara. The key is checked with the provider before it is saved.
| Field | Type | Required | Meaning |
|---|---|---|---|
apiKey | string | Yes | The provider credential |
clientId | string | Airwallex | Airwallex client ID |
externalAccountId | string | Airwallex | Airwallex account ID, used to route its events |
domain | string | Odoo | Instance domain, for example mycompany.odoo.com. Other providers reject it |
accountId | string | Avalara | Avalara account ID. Must not contain : |
environment | string | Avalara | production or sandbox |
companyCode | string | No | Avalara only. Up to 50 characters; an empty string clears it |
connectionId | string (UUID) | No | Update this existing connection instead of creating one |
name | string | No | Display name |
curl -X POST 'https://workflow.loopfour.ai/api/v1/connections/rillet/api-key' \
-H 'x-api-key: wfk_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{ "apiKey": "your_rillet_api_key", "name": "Rillet - main ledger" }'The response is 201 with the new connection, or 200 when connectionId updated one. A rejected key returns 400 (Invalid API key for rillet). If the provider cannot be reached to check the key, the request returns 502.
Update a connection
PATCH /api/v1/connections/{id}| Field | Type | Meaning |
|---|---|---|
name | string | New display name |
metadata | object | Keys to merge into the stored metadata (new keys win) |
Send at least one field. Keys that belong to the provider's credential flow cannot be changed here and return 400.
Set the default
POST /api/v1/connections/{id}/set-defaultMakes this connection the default for its provider. Steps that do not name a connection use the default. The response has id, name, provider, isDefault and message.
Refresh and verify
POST /api/v1/connections/{id}/refresh
POST /api/v1/connections/{id}/verifyRefresh renews the stored credentials and returns id, lastSyncedAt and message.
Verify tests the credentials against the provider and updates the status when the result is certain. A provider that cannot be reached does not mark the connection as broken.
{
"success": true,
"data": {
"healthy": true,
"connectionId": "8f14e45f-ceea-4e7a-9b1d-3c2a5d6e7f80",
"provider": "quickbooks",
"status": "active",
"externalAccountId": "9130354517268936",
"error": null,
"checkedAt": "2026-02-12T09:00:00.000Z"
}
}Delete a connection
DELETE /api/v1/connections/{id}Revokes access at the provider and removes the connection. If it was the default, another connection of the same provider becomes the default and is named in the response:
{
"success": true,
"data": {
"deleted": true,
"retained": false,
"promotedDefaultConnectionId": "2c8a4f1e-6b3d-4e7a-9f20-1d5c8b7a6e43",
"promotedDefaultConnectionName": "QuickBooks - UK entity"
}
}retained is true for Plaid, whose record is kept as disconnected. An abandoned connect attempt returns 409. If the provider cannot confirm the revoke, the connection is marked disconnected and the request returns 502.
Proxy a request to the provider
POST /api/v1/proxySends one request to the provider's API as the connection's account. Loopfour adds the account's credentials; you never handle them. Needs connections:write and counts against the write rate limit (100 requests a minute).
| Body field | Type | Meaning |
|---|---|---|
connectionId | string (UUID) | Required. An active connection of your company |
method | string | Required. GET, POST, PUT, PATCH or DELETE |
path | string | Required. The provider API path, for example /v3/company/<realm id>/invoice for QuickBooks or /v1/invoices for Stripe |
params | object | Query parameters, string values |
body | any | Request body, sent as JSON unless contentType is form |
contentType | string | json (default) or form, which sends application/x-www-form-urlencoded (Stripe) |
headers | object | Extra headers for the provider, string values |
baseUrl | string (URL) | Overrides the provider host, for providers whose host differs per account |
curl -X POST "https://workflow.loopfour.ai/api/v1/proxy" \
-H "x-api-key: wfk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"connectionId": "<connection-id>",
"method": "POST",
"path": "/v1/invoices/<invoice-id>",
"contentType": "form",
"body": { "metadata[external_id]": "1234" }
}'A success returns the provider's response body unchanged, not wrapped in { success, data }. A connection from another company returns 404; one that is not active returns 400. A GET, PUT or DELETE is retried on a timeout, 429 or 5xx; a POST or PATCH is retried only on 429, so a write is not repeated.
When the provider refuses the request, the response is 502 with UPSTREAM_ERROR. The message carries the provider's status and its own reason, read from the provider's JSON error body (for example a QuickBooks Fault, a Stripe error, or a top-level message), capped at 300 characters with tokens and keys removed. details.providerStatus carries the status and details.providerCode the error code, when there is one. A rate-limited provider's Retry-After is passed on. When the connection itself could not be used (for example its credentials can no longer be refreshed), the message says so and asks you to reconnect it, and details.providerCode names the reason, such as invalid_credentials. A body that is not a JSON error gets the status only. A 502 is not a reason to retry a write: check details.providerStatus first.
{
"success": false,
"error": {
"code": "UPSTREAM_ERROR",
"message": "The quickbooks request failed with status 400: ValidationFault: A business validation error has occurred while processing your request: <detail> (code 6000)",
"requestId": "<request-id>",
"details": { "providerStatus": 400, "providerCode": "6000" }
}
}Errors
| Status | Code | Cause |
|---|---|---|
400 | BAD_REQUEST | Missing provider-specific field, rejected API key, invalid cursor or connectionConfig, provider-owned metadata |
400 | ZodError in error.name | Body or query does not match the schema |
401 | UNAUTHORIZED | Missing or invalid API key |
403 | FORBIDDEN | Key lacks the scope |
404 | NOT_FOUND | Connection not found |
409 | CONFLICT | The connection attempt was never completed, or it changed during the request |
502 | UPSTREAM_ERROR | The provider could not be reached, or refused a proxied request (details.providerStatus) |
For connecting systems from Studio, see Connect a system.
Dashboards API
Read dashboards with the Dashboards API, see which changes need the owner, and manage widgets, layout and sharing with each endpoint.
Workflow Secrets API
Store, list and delete workflow secrets with the Workflow Secrets API, so credentials stay out of step settings and resolve at run time.
Loopfour