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/connectionsQuery Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
provider | string | - | Filter by provider: stripe, salesforce, hubspot, etc. |
status | string | - | Filter: active, pending, error, expired, disconnected, abandoned |
limit | number | 20 | Number 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-urlPath Parameters
| Parameter | Type | Description |
|---|---|---|
provider | string | Provider name: stripe, salesforce, hubspot, quickbooks, pandadoc, netsuite, slack, gmail |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
redirectUrl | string | No | URL to redirect after OAuth completion |
connectionName | string | No | Custom 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/callbackThis endpoint does not require authentication as it handles redirects from the OAuth provider.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
connectionId | string | The connection ID from the auth flow |
success | string | "true" or "false" |
error | string | Error message if authorization failed |
redirectUrl | string | URL to redirect the user to |
Successful Authorization
If redirectUrl is provided, redirects to:
https://app.example.com/settings/connections?success=true&connectionId=xxx&provider=stripeOtherwise 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=xxxCreate 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/connectionsRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name for the connection |
provider | string | Yes | Provider name |
metadata | object | No | Additional 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-keySupported 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
| Field | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Provider credential. For Avalara, this is the License Key. |
name | string | No | Display name for the connection |
connectionId | UUID | No | Replace credentials for this existing connection instead of creating one |
clientId | string | Airwallex only | Airwallex Client ID |
externalAccountId | string | Airwallex only | Airwallex Account ID used for webhook routing |
domain | string | Odoo only | Odoo instance domain, such as mycompany.odoo.com |
accountId | string | Avalara only | Avalara Account ID |
environment | string | Avalara only | production or sandbox |
companyCode | string | No | Optional 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/:idResponse
{
"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/:idResponse
{
"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/refreshResponse
{
"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/verifyResponse (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
| Status | Description |
|---|---|
pending | OAuth flow started but not completed |
active | Connection is working properly |
error | Connection has an issue (check errorMessage) |
expired | Needs 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 |
disconnected | User revoked access or connection was disconnected |
abandoned | A 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
| Provider | OAuth | Description |
|---|---|---|
stripe | Yes | Payment processing |
salesforce | Yes | CRM |
hubspot | Yes | Marketing/CRM |
quickbooks | Yes | Accounting |
pandadoc | Yes | Document signing |
netsuite | Yes | ERP |
slack | Yes | Messaging |
gmail | Yes | Email (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 redirectUrlExample 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."
}
}
Loopfour