Skip to content

Third-Party Service Integrations

Integration Architecture

Third-party service integrations in flow8 follow a consistent pattern:

User/Admin flow8 External Service
│ │ │
│──────────────────────→│ │
│ "Connect Slack" │ │
│ │──── Redirect to Auth ───→│
│ │ (OAuth2 flow) │
│ │←───── Auth Code ────────│
│ │ │
│ │─────────────────────────→│
│ │ Exchange code for token │
│ │←──── Access Token ──────│
│ │ │
│←──────────────────────│ │
│ "Connected!" │ │
│ (store encrypted) │ │

Integration Types

OAuth2 Integrations

These integrations use delegated authentication via OAuth2:

ServiceFlowScopesRefreshNotes
Microsoft/auth/callbackMail.Send, Mail.ReadWrite, Calendars.ReadWriteYes (auto-refresh)Azure AD integration, auto-user creation
Google/auth/callbackgmail.send, drive, sheetsYes (auto-refresh)Broad API coverage, reliable
Slack/auth/callbackchat:write, files:writeYes (auto-refresh)Simple bot API, good for notifications
Clio/auth/callbackclient.read, document.readNo (long-lived)Legal CRM, manual refresh needed
QuickBooks Online/auth/callbackquickbooksYes (refresh token)Accounting, Intuit OAuth
Bexio/auth/callbackclient.read, contact.readNo (long-lived)Swiss accounting software

API Key Integrations

These use API keys stored securely:

ServiceAuthCredential TypeNotes
AWS S3IAMAccess Key ID + Secret KeyUsed for storage, encrypted in DB
AnthropicAPI KeyAPI KeyDirect API access
OpenAIAPI KeyAPI KeyDirect API access
SMTPBasic AuthUsername + PasswordEmail sending
FTPBasic AuthUsername + PasswordFile transfer
HTTP APIsVariousAPI Key / TokenCustom integrations
type DBLink struct {
ID primitive.ObjectID
UserID primitive.ObjectID
CompanyID primitive.ObjectID
IntegrationType string // "microsoft", "slack", "google", etc.
AccessToken string // [encrypted]
RefreshToken string // [encrypted]
TokenExpiry time.Time
Scopes []string
ProviderUserID string // Unique ID from provider
ProviderUserEmail string // Email from provider
ProviderMetadata map[string]interface{} // Additional data
WebhookSignatureSecret string // [encrypted] For webhook validation
CreatedAt time.Time
UpdatedAt time.Time
}

Creating OAuth2 Integration

1. User initiates connection:

Terminal window
GET /api/v1/integrations/authorize?service=slack
Response:
{
"auth_url": "https://slack.com/oauth/v2/authorize?client_id=...&redirect_uri=..."
}

2. User is redirected to service (browser):

https://slack.com/oauth/v2/authorize
?client_id=123...
&redirect_uri=https://app.flow8.io/auth/callback
&scope=chat:write,files:write
&state=abc123... (CSRF protection)

3. User authorizes, service redirects back with code:

https://app.flow8.io/auth/callback
?code=xoxb-123...
&state=abc123

4. flow8 exchanges code for token:

pkg/service/integration_service.go
func (s *IntegrationService) ExchangeOAuthCode(ctx context.Context, code string, service string) (*DBLink, error) {
// Exchange code for token
token, err := s.slackClient.Exchange(ctx, code)
// Decrypt, verify, store
link := &DBLink{
IntegrationType: "slack",
AccessToken: encrypt(token.AccessToken),
RefreshToken: encrypt(token.RefreshToken),
TokenExpiry: token.Expiry,
ProviderUserID: token.SlackUserID,
UserID: ctx.Value("user_id").(primitive.ObjectID),
CompanyID: ctx.Value("company_id").(primitive.ObjectID),
}
s.db.Collection("db_links").InsertOne(ctx, link)
return link, nil
}

5. Link is stored encrypted in MongoDB:

{
"_id": ObjectId("..."),
"user_id": ObjectId("..."),
"company_id": ObjectId("..."),
"integration_type": "slack",
"access_token": {
"encrypted": true,
"nonce": "...",
"ciphertext": "..."
},
"refresh_token": {
"encrypted": true,
"nonce": "...",
"ciphertext": "..."
},
"token_expiry": ISODate("2026-07-04T10:00:00Z"),
"scopes": ["chat:write", "files:write"],
"provider_user_id": "U123...",
"provider_user_email": "user@company.com",
"created_at": ISODate("2026-04-04T10:00:00Z")
}

Token Refresh

Tokens are automatically refreshed before expiry:

func (s *IntegrationService) RefreshTokenIfNeeded(ctx context.Context, link *DBLink) error {
// Check if token expires within 1 hour
if time.Until(link.TokenExpiry) > 1*time.Hour {
return nil // Token still valid
}
// Refresh token
newToken, err := s.slackClient.RefreshToken(ctx, decrypt(link.RefreshToken))
// Update encrypted tokens
link.AccessToken = encrypt(newToken.AccessToken)
link.RefreshToken = encrypt(newToken.RefreshToken)
link.TokenExpiry = newToken.Expiry
s.db.Collection("db_links").UpdateByID(ctx, link.ID, link)
return nil
}

Unlinking Integration

Terminal window
DELETE /api/v1/integrations/slack/:link_id
# Result:
# 1. DBLink deleted
# 2. Tokens revoked (best effort)
# 3. Flows using this integration get access_denied

API Key Integrations

Storing API Keys

API keys are encrypted and stored in component configs or integration-specific documents:

Component config (for AI/Storage):

{
"_id": ObjectId("..."),
"name": "aws-s3-prod",
"kind": "storage",
"company_id": ObjectId("..."),
"config": {
"provider": "s3",
"access_key_id": {
"encrypted": true,
"nonce": "...",
"ciphertext": "..."
},
"secret_access_key": {
"encrypted": true,
"nonce": "...",
"ciphertext": "..."
}
}
}

REST API (creation):

Terminal window
POST /api/v1/admin/components
{
"name": "aws-s3-prod",
"kind": "storage",
"config": {
"provider": "s3",
"bucket": "flow8-artifacts",
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}
}
# Response (no credentials in response):
{
"id": "component_123",
"name": "aws-s3-prod",
"kind": "storage",
"config_summary": {
"provider": "s3",
"bucket": "flow8-artifacts",
"region": "us-east-1"
},
"is_default": false
}

Using API Keys in Modules

Modules receive decrypted credentials at runtime:

pkg/plugins/layers/v2/module_http_request.go
func (m *HTTPRequestModule) Execute(params Params) Response {
// Get component config
component := params.GetComponent("request")
// Credentials are already decrypted by framework
timeout := component.Config["timeout_seconds"]
maxRetries := component.Config["max_retries"]
// Make request with decrypted creds
// ...
}

Specific Integration Guides

Microsoft Graph (Office 365)

Setup:

config/config.yml
oauth2:
microsoft:
client_id: "550e8400-e29b-41d4-a716-446655440000"
client_secret: "[encrypted]"
redirect_uri: "https://app.flow8.io/auth/callback"
scopes:
- openid
- profile
- email
- Mail.Send
- Calendars.ReadWrite
auto_create_users: true

Capabilities:

  • Send email via Outlook
  • Read mail (delta query for incremental sync)
  • Create calendar events
  • List contacts
  • Read OneDrive files

Module integration:

{
"name": "send-meeting-invite",
"module_ref": "microsoft-calendar",
"input_mapping": {
"link_id": "link_123",
"event_title": "flow8 Demo",
"attendees": ["user@company.com"],
"start_time": "2026-04-05T10:00:00Z",
"end_time": "2026-04-05T11:00:00Z"
}
}

AWS S3

Configuration:

{
"name": "s3-prod",
"kind": "storage",
"config": {
"provider": "s3",
"bucket": "flow8-artifacts",
"region": "us-east-1",
"access_key_id": "[encrypted]",
"secret_access_key": "[encrypted]"
}
}

Usage:

{
"name": "upload-document",
"module_ref": "s3-upload",
"input_mapping": {
"bucket": "flow8-artifacts",
"key": "invoices/2026-04/invoice-001.pdf",
"file_path": "/tmp/invoice.pdf",
"acl": "private"
}
}

Slack

Setup:

Terminal window
# 1. Create Slack App at https://api.slack.com/apps
# 2. Set redirect URL: https://app.flow8.io/auth/callback
# 3. Request OAuth scopes: chat:write, files:write
# 4. Copy Client ID and Secret to .env
# 5. User connects via UI

Usage:

{
"name": "notify-slack",
"module_ref": "slack-message",
"input_mapping": {
"link_id": "link_slack_123",
"channel": "#notifications",
"message": "Invoice #001 processed successfully",
"blocks": [
{
"type": "section",
"text": { "type": "mrkdwn", "text": "Invoice Processed" }
}
]
}
}

Google Drive/Sheets

Capabilities:

  • Upload files to Google Drive
  • Create Google Sheets
  • Write data to Sheets
  • Read Sheet data
  • Share files with collaborators

Configuration:

Terminal window
# Google Cloud Console setup
# - Create OAuth 2.0 credentials (Web application)
# - Add redirect URI: https://app.flow8.io/auth/callback
# - Scopes: drive, sheets, gmail

Custom APIs

For HTTP-based APIs, use the generic HTTP Request module:

{
"name": "call-custom-api",
"module_ref": "http-request",
"input_mapping": {
"url": "https://api.example.com/process",
"method": "POST",
"headers": {
"Authorization": "Bearer token_123",
"Content-Type": "application/json"
},
"body": {
"document_id": "doc_001",
"action": "analyze"
}
}
}

Credential Rotation

Rotating OAuth2 Tokens

For long-lived access, manually revoke and reconnect:

Terminal window
# Revoke all tokens for a service
DELETE /api/v1/integrations/links?service=slack
# User must reconnect to re-authorize

Rotating API Keys

Terminal window
# 1. Create new component with new API key
curl -X POST http://localhost:4454/api/v1/admin/components \
-d '{"name": "aws-s3-prod-v2", "config": {"access_key_id": "new_key", ...}}'
# 2. Update flows to reference new component
# 3. Revoke old API key in provider console
# 4. Delete old component

Integration Health Monitoring

Testing Connectivity

Terminal window
# Test OAuth2 link
curl -X POST http://localhost:4454/api/v1/integrations/test \
-H "Authorization: Bearer $TOKEN" \
-d '{"link_id": "link_123"}'
Response:
{
"status": "ok",
"service": "slack",
"latency_ms": 123,
"token_valid": true,
"scopes_granted": ["chat:write", "files:write"],
"user": "U123..."
}

Monitoring Token Expiry

Terminal window
# Query links expiring within 7 days
db.db_links.find({
token_expiry: {
$gt: new Date(),
$lt: new Date(Date.now() + 7*24*60*60*1000)
}
})

Audit Logging

All integration events are audited:

{
"action_type": "integration_linked",
"resource_type": "integration",
"resource_id": "link_123",
"before_state": {},
"after_state": {
"integration_type": "slack",
"provider_user_id": "U123...",
"scopes": ["chat:write", "files:write"]
},
"timestamp": "2026-04-04T10:00:00Z"
}

Security Best Practices

  1. Encrypt all credentials at rest: NaCl SecretBox ensures encrypted storage

  2. Use short-lived tokens: OAuth2 access tokens expire quickly; refresh tokens are stored encrypted

  3. Validate API key format: Reject malformed keys before storing

  4. Monitor token usage: Alert on unusual API activity

  5. Rotate keys regularly: Quarterly rotation recommended

  6. Revoke immediately: If key is compromised, revoke before attackers use it

  7. Use scopes minimally: Request only necessary permissions (principle of least privilege)

  8. Test integrations regularly: Verify tokens still work via health checks

Troubleshooting

”Invalid OAuth Code”

Error: invalid_grant

Solution:

  • Code expires after 10 minutes
  • Code can only be used once
  • Ensure redirect_uri matches exactly
  • Verify client_id and client_secret

”Access Denied” in Module

Error: insufficient_scope

Solution:

  • Reconnect integration to request missing scopes
  • Verify user authorized all required permissions
  • Check token expiry (may need refresh)

“Token Revoked”

Error: token_revoked

Solution:

  • User revoked token in provider (e.g., Slack, Google settings)
  • Admin revoked token in flow8
  • User must reconnect

Rate Limit from External Service

Error: rate_limit_exceeded (Slack API)

Solution:

  • Implement exponential backoff in module
  • Reduce concurrent executions
  • Contact provider for higher limits
  • Use batching APIs where available