Loopfour
API reference

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.

ScopeEndpoints
connections:readList, get, verify
connections:writeCreate, auth URL, update, set default, refresh, delete, API key connection, proxy a request

Credentials are never returned. Metadata in responses is redacted.

Connection object

FieldTypeMeaning
idstring (UUID)Connection ID. Use it as config.connection in a step
namestringDisplay name, up to 255 characters
providerstringProvider ID, for example quickbooks
statusstringpending, active, expired, error, disconnected or abandoned
externalAccountIdstring or nullThe account ID in the other system
errorMessagestring or nullList only. Last error
isDefaultbooleanWhether steps use it when they do not name a connection
authVariantstring or nullThe sign-in method, for providers that offer more than one
lastSyncedAtstring or null
metadataobjectGet and update only. Redacted
createdAt, updatedAtstring (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 parameterTypeDefaultMeaning
providerstringA provider ID
statusstringA status. Without it, abandoned attempts are left out
limitinteger201 to 100
cursorstringmeta.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
FieldTypeRequiredMeaning
namestringYes1 to 255 characters
providerstringYesA provider ID
isDefaultbooleanNoMake it the default for the provider. The first connection of a provider becomes the default
metadataobjectNo
connectionConfigobjectNoProvider-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 parameterTypeMeaning
redirectUrlstring (URL)Where to send the person afterwards
connectionNamestringDisplay name. Defaults to a name based on the provider
connectionConfigstringProvider-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-key

provider is rillet, airwallex, odoo or avalara. The key is checked with the provider before it is saved.

FieldTypeRequiredMeaning
apiKeystringYesThe provider credential
clientIdstringAirwallexAirwallex client ID
externalAccountIdstringAirwallexAirwallex account ID, used to route its events
domainstringOdooInstance domain, for example mycompany.odoo.com. Other providers reject it
accountIdstringAvalaraAvalara account ID. Must not contain :
environmentstringAvalaraproduction or sandbox
companyCodestringNoAvalara only. Up to 50 characters; an empty string clears it
connectionIdstring (UUID)NoUpdate this existing connection instead of creating one
namestringNoDisplay 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}
FieldTypeMeaning
namestringNew display name
metadataobjectKeys 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-default

Makes 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}/verify

Refresh 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/proxy

Sends 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 fieldTypeMeaning
connectionIdstring (UUID)Required. An active connection of your company
methodstringRequired. GET, POST, PUT, PATCH or DELETE
pathstringRequired. The provider API path, for example /v3/company/<realm id>/invoice for QuickBooks or /v1/invoices for Stripe
paramsobjectQuery parameters, string values
bodyanyRequest body, sent as JSON unless contentType is form
contentTypestringjson (default) or form, which sends application/x-www-form-urlencoded (Stripe)
headersobjectExtra headers for the provider, string values
baseUrlstring (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

StatusCodeCause
400BAD_REQUESTMissing provider-specific field, rejected API key, invalid cursor or connectionConfig, provider-owned metadata
400ZodError in error.nameBody or query does not match the schema
401UNAUTHORIZEDMissing or invalid API key
403FORBIDDENKey lacks the scope
404NOT_FOUNDConnection not found
409CONFLICTThe connection attempt was never completed, or it changed during the request
502UPSTREAM_ERRORThe provider could not be reached, or refused a proxied request (details.providerStatus)

For connecting systems from Studio, see Connect a system.

On this page