Loopfour
API Reference

Connections API

Manage OAuth and API key connections for integrations

Connections API

Manage OAuth connections to external services. Connections are used by workflows to authenticate with integrated providers like Stripe, Salesforce, HubSpot, and more.

List Connections

Retrieve all connections for your company.

GET /api/v1/connections

Query Parameters

ParameterTypeDefaultDescription
providerstring-Filter by provider: stripe, salesforce, hubspot, etc.
statusstring-Filter: active, pending, error, expired, disconnected, abandoned
limitnumber20Number of results (1-100)

An unfiltered list excludes abandoned connections. They are audit records of connect attempts that never held a credential, they accumulate without bound, and leaving them in would let them consume the page limit and hide live connections. Pass ?status=abandoned to read them.

Response

{
  "success": true,
  "data": [
    {
      "id": "conn_550e8400-e29b-41d4-a716-446655440000",
      "name": "Stripe Production",
      "provider": "stripe",
      "status": "active",
      "externalAccountId": "acct_xxx",
      "lastSyncedAt": "2024-01-15T10:30:00.000Z",
      "createdAt": "2024-01-01T10:00:00.000Z",
      "updatedAt": "2024-01-15T10:30:00.000Z"
    },
    {
      "id": "conn_660e8400-e29b-41d4-a716-446655440001",
      "name": "HubSpot CRM",
      "provider": "hubspot",
      "status": "active",
      "externalAccountId": "123456",
      "lastSyncedAt": "2024-01-15T09:00:00.000Z",
      "createdAt": "2024-01-05T10:00:00.000Z",
      "updatedAt": "2024-01-15T09:00:00.000Z"
    }
  ],
  "meta": {
    "hasMore": false
  }
}

Example

curl -X GET "https://workflow.loopfour.ai/api/v1/connections?provider=stripe" \
  -H "x-api-key: YOUR_API_KEY"
const response = await fetch(
  'https://workflow.loopfour.ai/api/v1/connections?provider=stripe',
  {
    headers: { 'x-api-key': process.env.API_KEY }
  }
);
const { data } = await response.json();
import requests

response = requests.get(
    'https://workflow.loopfour.ai/api/v1/connections',
    params={'provider': 'stripe'},
    headers={'x-api-key': api_key}
)
data = response.json()

Get Auth URL

Generate an OAuth authorization URL for a provider. This creates a pending connection and returns the URL to redirect the user to.

This endpoint accepts OAuth providers only. Direct API-key providers (rillet, resend, sendgrid, airwallex, odoo, and avalara) return 400 Bad Request; use the API-key endpoint documented below instead.

GET /api/v1/connections/:provider/auth-url

Path Parameters

ParameterTypeDescription
providerstringProvider name: stripe, salesforce, hubspot, quickbooks, pandadoc, netsuite, slack, gmail

Query Parameters

ParameterTypeRequiredDescription
redirectUrlstringNoURL to redirect after OAuth completion
connectionNamestringNoCustom name for the connection

Response

{
  "success": true,
  "data": {
    "connectionId": "conn_550e8400-e29b-41d4-a716-446655440000",
    "provider": "stripe",
    "authUrl": "https://connect.stripe.com/oauth/authorize?client_id=xxx&redirect_uri=xxx&state=xxx",
    "isNew": true,
    "message": "Redirect user to authUrl to complete OAuth authorization"
  }
}

Example

curl -X GET "https://workflow.loopfour.ai/api/v1/connections/stripe/auth-url?redirectUrl=https://app.example.com/settings/connections" \
  -H "x-api-key: YOUR_API_KEY"

Re-authorization

If a connection already exists but is in error or disconnected status, calling this endpoint will return a new auth URL for re-authorization:

{
  "success": true,
  "data": {
    "connectionId": "conn_existing",
    "provider": "stripe",
    "authUrl": "https://connect.stripe.com/oauth/authorize?...",
    "isNew": false,
    "message": "Redirect user to authUrl to complete OAuth authorization"
  }
}

Error Response (Active Connection Exists)

{
  "success": false,
  "error": {
    "code": "BAD_REQUEST",
    "message": "Active connection already exists for stripe. Disconnect first to re-authorize."
  }
}

OAuth Callback

Handle the OAuth callback from the provider. This endpoint is called by Nango after the user completes authorization.

GET /api/v1/connections/:provider/callback

This endpoint does not require authentication as it handles redirects from the OAuth provider.

Query Parameters

ParameterTypeDescription
connectionIdstringThe connection ID from the auth flow
successstring"true" or "false"
errorstringError message if authorization failed
redirectUrlstringURL to redirect the user to

Successful Authorization

If redirectUrl is provided, redirects to:

https://app.example.com/settings/connections?success=true&connectionId=xxx&provider=stripe

Otherwise returns:

{
  "success": true,
  "data": {
    "connectionId": "conn_xxx",
    "provider": "stripe",
    "status": "active",
    "message": "OAuth authorization completed successfully"
  }
}

Failed Authorization

If redirectUrl is provided, redirects to:

https://app.example.com/settings/connections?error=access_denied&connectionId=xxx

Create Connection

Initiate a new OAuth connection. Direct API-key providers return 400 Bad Request and must use POST /api/v1/connections/:provider/api-key instead.

POST /api/v1/connections

Request Body

FieldTypeRequiredDescription
namestringYesDisplay name for the connection
providerstringYesProvider name
metadataobjectNoAdditional metadata

Example Request

{
  "name": "Production Stripe",
  "provider": "stripe",
  "metadata": {
    "environment": "production"
  }
}

Response (201 Created)

{
  "success": true,
  "data": {
    "id": "conn_550e8400-e29b-41d4-a716-446655440000",
    "name": "Production Stripe",
    "provider": "stripe",
    "status": "pending",
    "oauthUrl": "https://api.nango.dev/oauth/connect/stripe?connection_id=xxx&public_key=xxx",
    "message": "Redirect user to oauthUrl to complete connection"
  }
}

Create API-Key Connection

Create a direct API-key connection without starting an OAuth flow.

POST /api/v1/connections/:provider/api-key

Supported providers are rillet, resend, sendgrid, airwallex, odoo, and avalara. The request requires the same x-api-key authorization and connections:write scope as OAuth connection creation.

Request Body

FieldTypeRequiredDescription
apiKeystringYesProvider credential. For Avalara, this is the License Key.
namestringNoDisplay name for the connection
connectionIdUUIDNoReplace credentials for this existing connection instead of creating one
clientIdstringAirwallex onlyAirwallex Client ID
externalAccountIdstringAirwallex onlyAirwallex Account ID used for webhook routing
domainstringOdoo onlyOdoo instance domain, such as mycompany.odoo.com
accountIdstringAvalara onlyAvalara Account ID
environmentstringAvalara onlyproduction or sandbox
companyCodestringNoOptional Avalara company code

SendGrid Example

curl -X POST "https://workflow.loopfour.ai/api/v1/connections/sendgrid/api-key" \
  -H "x-api-key: YOUR_WORKFLOWS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"apiKey":"YOUR_SENDGRID_API_KEY","name":"SendGrid Production"}'

The endpoint returns 201 Created for a new connection and 200 OK when connectionId replaces an existing connection's credentials. Invalid credentials return 400 Bad Request; an unreachable provider returns 502 Bad Gateway.

Get Connection

Retrieve details of a specific connection.

GET /api/v1/connections/:id

Response

{
  "success": true,
  "data": {
    "id": "conn_550e8400-e29b-41d4-a716-446655440000",
    "name": "Stripe Production",
    "provider": "stripe",
    "status": "active",
    "externalAccountId": "acct_xxx",
    "lastSyncedAt": "2024-01-15T10:30:00.000Z",
    "metadata": {
      "environment": "production"
    },
    "createdAt": "2024-01-01T10:00:00.000Z",
    "updatedAt": "2024-01-15T10:30:00.000Z"
  }
}

Delete Connection

Revoke and delete a connection. This also removes the connection from Nango.

DELETE /api/v1/connections/:id

Response

{
  "success": true,
  "data": {
    "deleted": true
  }
}

Deleting a connection will cause workflows using this connection to fail. Make sure to update or pause affected workflows first.

Refresh Connection

Manually refresh connection credentials. Useful when credentials are about to expire.

POST /api/v1/connections/:id/refresh

Response

{
  "success": true,
  "data": {
    "id": "conn_550e8400-e29b-41d4-a716-446655440000",
    "lastSyncedAt": "2024-01-15T10:35:00.000Z",
    "message": "Connection refresh completed"
  }
}

Error Response (Refresh Failed)

If the refresh fails, the connection status is updated to error:

{
  "success": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Failed to refresh credentials: Token expired"
  }
}

Verify Connection

Test connection health by verifying credentials with the provider.

POST /api/v1/connections/:id/verify

Response (Healthy Connection)

{
  "success": true,
  "data": {
    "healthy": true,
    "connectionId": "conn_550e8400-e29b-41d4-a716-446655440000",
    "provider": "stripe",
    "status": "active",
    "externalAccountId": "acct_xxx",
    "error": null,
    "checkedAt": "2024-01-15T10:35:00.000Z"
  }
}

Response (Unhealthy Connection)

{
  "success": true,
  "data": {
    "healthy": false,
    "connectionId": "conn_550e8400-e29b-41d4-a716-446655440000",
    "provider": "stripe",
    "status": "error",
    "externalAccountId": null,
    "error": "Invalid API key provided",
    "checkedAt": "2024-01-15T10:35:00.000Z"
  }
}

Response (Provider Unreachable)

When the provider itself could not be reached — a timeout, a DNS failure, a transport error — the check returns healthy: false without changing the stored status: an unreachable provider is not a verdict on your credentials, and marking the connection error would stop every workflow step that uses it until someone re-entered a secret the UI does not display again.

{
  "success": true,
  "data": {
    "healthy": false,
    "connectionId": "conn_550e8400-e29b-41d4-a716-446655440000",
    "provider": "avalara",
    "status": "active",
    "error": "Could not reach Avalara to verify this connection. Try again in a moment.",
    "checkedAt": "2024-01-15T10:35:00.000Z"
  }
}

status echoes the row's unchanged value. Retry the check once the provider is responding.

Response (Not Yet Authorized)

{
  "success": true,
  "data": {
    "healthy": false,
    "connectionId": "conn_550e8400-e29b-41d4-a716-446655440000",
    "provider": "stripe",
    "status": "pending",
    "error": "Connection not yet authorized - complete OAuth flow first",
    "checkedAt": "2024-01-15T10:35:00.000Z"
  }
}

Connection Statuses

StatusDescription
pendingOAuth flow started but not completed
activeConnection is working properly
errorConnection has an issue (check errorMessage)
expiredNeeds re-authorization. For OAuth providers, the token expired or was revoked. For Financial Accounts (plaid), the bank requires a fresh sign-in (shown as Reconnect required); the provider error code is lastErrorCode on GET /api/v1/connections/:id/plaid/accounts
disconnectedUser revoked access or connection was disconnected
abandonedA connect attempt that never completed (the user closed Link, or removed the pending attempt); retained for audit, never usable, and excluded from an unfiltered list

Supported Providers

ProviderOAuthDescription
stripeYesPayment processing
salesforceYesCRM
hubspotYesMarketing/CRM
quickbooksYesAccounting
pandadocYesDocument signing
netsuiteYesERP
slackYesMessaging
gmailYesEmail (Google)

OAuth Flow

The typical OAuth flow for connecting a provider:

1. Your app calls GET /api/v1/connections/:provider/auth-url
2. API returns authUrl and connectionId
3. Your app redirects user to authUrl
4. User authorizes in provider's UI
5. Provider redirects to Nango callback
6. Nango redirects to /api/v1/connections/:provider/callback
7. API updates connection status to 'active'
8. User is redirected to your redirectUrl

Example Flow (JavaScript)

// Step 1: Get auth URL
const authResponse = await fetch(
  `https://workflow.loopfour.ai/api/v1/connections/stripe/auth-url?redirectUrl=${encodeURIComponent('https://app.example.com/settings')}`,
  { headers: { 'x-api-key': API_KEY } }
);
const { data } = await authResponse.json();

// Step 2: Redirect user to OAuth
window.location.href = data.authUrl;

// Step 3: Handle callback (on your redirect page)
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('success') === 'true') {
  const connectionId = urlParams.get('connectionId');
  console.log('Connection created:', connectionId);
} else {
  const error = urlParams.get('error');
  console.error('OAuth failed:', error);
}

Error Responses

Connection Not Found (404)

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Connection not found"
  }
}

Active Connection Exists (400)

{
  "success": false,
  "error": {
    "code": "BAD_REQUEST",
    "message": "Active connection already exists for stripe. Disconnect first to re-authorize."
  }
}

Provider Unreachable (502)

Returned by the API-key connect endpoint when the provider could not be reached to check the credential. Distinct from BAD_REQUEST on purpose: the credential was never judged, so this is a signal to retry, not to rotate a secret.

{
  "success": false,
  "error": {
    "code": "UPSTREAM_ERROR",
    "message": "Could not reach Avalara to verify this connection. Try again in a moment."
  }
}

On this page