Skip to main content
Custom integrations allow you to extend Cargo’s capabilities by connecting to any external service or API. By building a custom integration server, you can create actions, data extractors, and autocomplete endpoints that seamlessly integrate with Cargo’s workflows.
Check out the dummy-integration repository for a complete working example of a custom integration.

Overview

A custom integration is an HTTP server that implements a specific API contract. Cargo communicates with your integration server to:
  • Fetch the manifest — Describes your integration’s capabilities (actions, extractors, autocompletes)
  • Authenticate connections — Validates user credentials when creating a connector
  • Execute actions — Performs operations in your external service
  • Fetch data — Pulls data from your service into Cargo data models
  • Provide autocomplete options — Powers dynamic dropdowns in the Cargo UI

Getting started

Step 1: Create your integration server

Start by cloning the dummy integration repository:
git clone https://github.com/getcargohq/dummy-integration.git
cd dummy-integration
npm install
Run the development server:
npm run dev
Your integration server will start on a local port (e.g., http://localhost:3000).

Step 2: Expose your local server with ngrok

During development, you can use ngrok to expose your local server to the internet:
ngrok http 3000
This will give you a public URL like https://abc123.ngrok.io that you can use to register your integration with Cargo.
For production, deploy your integration server to a cloud provider (AWS, GCP, Vercel, Railway, etc.) and use that URL instead.

Step 3: Register in Cargo via API

Custom integrations are registered using the Cargo API. You’ll need an API token. Create a custom integration:
curl -X POST https://api.getcargo.io/v1/connection/customIntegrations \
  -H "Authorization: Basic YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "baseUrl": "https://abc123.ngrok.io"
  }'
Once registered, Cargo will fetch the manifest from your server and the integration will appear in your workspace’s connector catalog.
Your integration server must be publicly accessible. Cargo’s backend needs to make HTTP requests to your server’s endpoints.

Cargo API for custom integrations

Manage your custom integrations using these API endpoints. All endpoints require authentication.

Create a custom integration

POST /v1/connection/customIntegrations
Request body:
{
  "baseUrl": "https://your-integration-server.com"
}
Response:
{
  "customIntegration": {
    "uuid": "ci_abc123",
    "baseUrl": "https://your-integration-server.com",
    "createdAt": "2025-01-01T00:00:00Z"
  }
}

List custom integrations

GET /v1/connection/customIntegrations/list
Response:
{
  "customIntegrations": [
    {
      "uuid": "ci_abc123",
      "baseUrl": "https://your-integration-server.com",
      "createdAt": "2025-01-01T00:00:00Z"
    }
  ]
}

Get a custom integration

GET /v1/connection/customIntegrations/:uuid
Response:
{
  "customIntegration": {
    "uuid": "ci_abc123",
    "baseUrl": "https://your-integration-server.com",
    "createdAt": "2025-01-01T00:00:00Z"
  }
}

Update a custom integration

PUT /v1/connection/customIntegrations/:uuid
Request body:
{
  "baseUrl": "https://new-integration-server.com"
}

Delete a custom integration

DELETE /v1/connection/customIntegrations/:uuid

Integration server API

Your integration server must implement the following HTTP endpoints:

GET /manifest

Returns the integration manifest describing all capabilities. Response:
{
  "name": "My Integration",
  "description": "A custom integration for my service",
  "icon": "<svg>...</svg>",
  "color": "#6366F1",
  "url": "https://myservice.com",
  "connector": {
    "config": {
      "jsonSchema": {
        "type": "object",
        "properties": {
          "apiKey": {
            "title": "API Key",
            "type": "string"
          }
        },
        "required": ["apiKey"]
      },
      "uiSchema": {}
    },
    "rateLimit": {
      "unit": "minute",
      "max": 60
    }
  },
  "actions": {
    "createRecord": {
      "name": "Create Record",
      "description": "Creates a new record in the service",
      "config": {
        "jsonSchema": {
          "type": "object",
          "properties": {
            "name": {
              "title": "Name",
              "type": "string"
            }
          },
          "required": ["name"]
        },
        "uiSchema": {}
      }
    }
  },
  "extractors": {
    "fetchRecords": {
      "name": "Fetch Records",
      "description": "Fetches records from the service",
      "config": {
        "jsonSchema": {},
        "uiSchema": {}
      },
      "mode": {
        "kind": "fetch",
        "isIncremental": false
      },
      "preview": "records"
    }
  },
  "autocompletes": {
    "listOptions": {
      "params": {
        "jsonSchema": {}
      },
      "cacheExpirationInSeconds": 300
    }
  }
}

POST /authenticate

Validates the connector configuration (credentials). Request body:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  }
}
Response (success):
{
  "outcome": "success"
}
Response (error):
{
  "outcome": "error",
  "reason": "unauthenticated",
  "errorMessage": "Invalid API key provided"
}

POST /listUsers

Lists users available in the connected service (optional). Request body:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  }
}
Response:
[
  {
    "id": "user_123",
    "email": "[email protected]",
    "firstName": "John",
    "lastName": "Doe",
    "profileImage": "https://example.com/avatar.png"
  }
]

POST /actions/[actionSlug]/execute

Executes an action. The actionSlug corresponds to a key in the actions object returned by your /manifest endpoint. Request body:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "actionConfig": {
    "name": "New Record Name"
  }
}
Response (completed):
{
  "outcome": "executed",
  "title": "Record created successfully",
  "data": {
    "id": "rec_123",
    "name": "New Record Name",
    "createdAt": "2025-01-01T00:00:00Z"
  }
}
Response (in progress):
{
  "outcome": "executing"
}

POST /extractors/[extractorSlug]/fetch

Fetches data for a data model extractor. Request body:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "extractorConfig": {},
  "meta": {}
}
Response:
{
  "outcome": "fetched",
  "columns": [
    {
      "slug": "id",
      "type": "string",
      "label": "ID"
    },
    {
      "slug": "name",
      "type": "string",
      "label": "Name"
    },
    {
      "slug": "createdAt",
      "type": "date",
      "label": "Created At"
    }
  ],
  "idColumnSlug": "id",
  "titleColumnSlug": "name",
  "uniqueColumns": [],
  "data": {
    "kind": "records",
    "records": [
      {
        "action": "upsert",
        "override": true,
        "record": {
          "id": "rec_123",
          "name": "Record 1",
          "createdAt": "2025-01-01T00:00:00Z"
        }
      }
    ],
    "hasMore": false
  }
}

POST /extractors/[extractor]/count

Returns the count of records for preview purposes. Response:
{
  "count": 150
}

POST /autocompletes/[autocompleteSlug]

Provides options for dynamic dropdowns in the UI. Request body:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "params": {}
}
Response:
{
  "results": [
    {
      "label": "Option 1",
      "value": "option_1",
      "description": "Description for option 1"
    },
    {
      "label": "Option 2",
      "value": "option_2"
    }
  ]
}

POST /dynamicSchemas/[schemaSlug]

Returns dynamic JSON schemas based on runtime parameters. Request body:
{
  "connectorConfig": {
    "apiKey": "encrypted_value"
  },
  "params": {}
}
Response:
{
  "jsonSchema": {
    "type": "object",
    "properties": {
      "dynamicField": {
        "title": "Dynamic Field",
        "type": "string"
      }
    }
  },
  "uiSchema": {}
}

POST /completeOauth

Completes OAuth flow for integrations using OAuth authentication. Request body:
{
  "code": "oauth_authorization_code",
  "redirectUri": "https://app.getcargo.io/oauth/callback"
}
Response:
{
  "value": "encrypted_oauth_token"
}

Using autocompletes in action/extractor forms

To power dynamic dropdowns in your action configuration forms, use the IntegrationAutocompleteWidget in your uiSchema. This widget calls your /autocompletes/{slug} endpoint to fetch options.

Basic usage

In your /manifest endpoint response, include an autocompletes entry and reference it in an action’s uiSchema:
{
  "autocompletes": {
    "listWorkspaces": {
      "params": {
        "jsonSchema": {}
      },
      "cacheExpirationInSeconds": 300
    }
  },
  "actions": {
    "syncData": {
      "name": "Sync Data",
      "description": "Sync data from a workspace",
      "config": {
        "jsonSchema": {
          "type": "object",
          "properties": {
            "workspaceId": {
              "type": "string",
              "title": "Workspace"
            }
          },
          "required": ["workspaceId"]
        },
        "uiSchema": {
          "workspaceId": {
            "ui:widget": "IntegrationAutocompleteWidget",
            "ui:options": {
              "slug": "listWorkspaces",
              "params": {}
            }
          }
        }
      }
    }
  }
}

Widget options

OptionTypeDescription
slugstringThe autocomplete slug (matches entry in /manifest response)
paramsobjectStatic or dynamic parameters to pass to the endpoint
allowRefreshbooleanShow a refresh button to reload options

Dynamic parameters

You can reference other form values in params using special path expressions:
{
  "uiSchema": {
    "accounts": {
      "ui:widget": "IntegrationAutocompleteWidget",
      "ui:options": {
        "slug": "listAccounts",
        "params": {
          "workspaceId": "$this.$parent.workspaceId"
        }
      }
    }
  }
}
Path expressions:
ExpressionDescription
$thisCurrent field value
$parentParent object in the form structure
$rootRoot of the form data
This allows cascading dropdowns where one field’s options depend on another field’s selected value.

Using dynamic schemas

For fields where the schema depends on runtime values (like user selections), use the DynamicSchemaWidget. This widget fetches the JSON schema from your /dynamicSchemas/{slug} endpoint.

Basic usage

In your /manifest endpoint response, include a dynamicSchemas entry and reference it in an action’s uiSchema:
{
  "dynamicSchemas": {
    "getFieldSchema": {
      "params": {
        "jsonSchema": {
          "type": "object",
          "properties": {
            "objectType": {
              "type": "string"
            }
          }
        }
      }
    }
  },
  "actions": {
    "createRecord": {
      "name": "Create Record",
      "description": "Create a record with dynamic fields",
      "config": {
        "jsonSchema": {
          "type": "object",
          "properties": {
            "objectType": {
              "type": "string",
              "title": "Object Type"
            },
            "fields": {
              "type": "object",
              "title": "Fields"
            }
          }
        },
        "uiSchema": {
          "objectType": {
            "ui:widget": "IntegrationAutocompleteWidget",
            "ui:options": {
              "slug": "listObjectTypes"
            }
          },
          "fields": {
            "ui:widget": "DynamicSchemaWidget",
            "ui:options": {
              "slug": "getFieldSchema",
              "params": {
                "objectType": "$this.$parent.objectType"
              }
            }
          }
        }
      }
    }
  }
}

Widget options

OptionTypeDescription
slugstringThe dynamic schema slug (matches entry in /manifest response)
paramsobjectParameters to pass to the endpoint (supports path expressions)

Endpoint response

Your /dynamicSchemas/{slug} endpoint should return both the JSON schema and UI schema for the field:
{
  "jsonSchema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "title": "Name"
      },
      "email": {
        "type": "string",
        "title": "Email"
      }
    }
  },
  "uiSchema": {}
}
Dynamic schemas are useful when your integration has different fields per object type (e.g., CRM objects like Contacts vs Companies) or when fields are user-configurable.

Best practices

Validate inputs

Always validate connector and action configurations before processing requests.

Handle errors gracefully

Return meaningful error messages to help users troubleshoot issues.

Implement rate limiting

Define rate limits in your /manifest response to prevent overwhelming your service.

Use caching

Enable caching for autocomplete endpoints to improve performance.

Example integration

For a complete working example, see the Cargo Dummy Integration repository on GitHub. The repository includes:
  • Full project structure with TypeScript
  • Example /manifest endpoint with actions and extractors
  • Authentication endpoint implementation
  • Action execution handlers
  • Development and build scripts