Skip to content

API Actions Architecture & Flow (Legacy) ​

⚠️ DEPRECATED: API Actions are being replaced by the unified Webhook system with Custom Payload Mode. New integrations should use Webhooks (Tools → Add Webhook → Custom mode) instead. Existing API Actions can be migrated via POST /migration/api-actions. The legacy API Action creation UI has been removed from the campaign upload page.

Campaign-specific outbound HTTP integrations: automatically push call data to external APIs (CRM, webhooks, workflows) when call events occur, with custom payloads using template interpolation.


What It Does ​

API Actions let customers define custom HTTP endpoints that are called automatically when call lifecycle events happen for a specific campaign. Unlike webhooks (which push a fixed payload to the customer's server), API Actions give full control over:

  • Which API to call (any HTTPS endpoint)
  • What data to send (custom JSON body with {{variable}} placeholders)
  • When to send it (per event type: call ended, transcription ready, analysis ready, etc.)
  • How to authenticate (Bearer, API Key, Basic Auth)
  • Which campaign triggers it (campaign-scoped, not global)

This is designed for users who want Oktivo to update their CRM, trigger a workflow, or sync data to their own systems after each call without building a webhook receiver.


How It Differs From Webhooks ​

FeatureWebhooksAPI Actions
DirectionOktivo → Customer's receiverOktivo → Customer's API
PayloadFixed schema (versioned)Custom body template with {{variables}}
ScopeGlobal (all campaigns)Campaign-specific
AuthHMAC-SHA256 signatureBearer / API Key / Basic Auth
ConfigurationSeparate Integrations pageInline during campaign upload
Use case"Notify me when events happen""Update my CRM with call results"

Both systems share the same event types and are triggered from the same lifecycle hooks.


Event Types ​

EventFires WhenTypical Use
call.endedCall disconnectsUpdate CRM call status
call.transcription_readyTranscription stored on ContactPush conversation text to CRM
call.analysis_readyAnalysis stored on ContactUpdate lead score / sentiment
call.recording_readyRecording URL availableStore recording link in CRM
call.completedAll 4 above are presentFull sync all data in one payload

Timing note: Analysis takes 5–10 seconds after call end. The call.analysis_ready event fires only after analysis is written to the Contact document. call.completed fires only when ALL data points (call status + transcription + analysis + recording) are present whichever lifecycle hook runs last triggers it.


Architecture ​


Campaign Lifecycle Full API Action Timeline ​


Template Interpolation ​

API Action body templates use {{variable}} Handlebars-style placeholders that are resolved from Contact data at execution time.

System Variables (always available) ​

VariableSourceExample Value
{{phone}}Contact.phone+919876543210
{{name}}Contact.nameRahul Sharma
{{contactUID}}Contact.contactUIDETD1RtRBREAj
{{campaignUID}}Contact.campaignUIDD-NzcJnBLqnx
{{agentId}}Contact.AgentIDWtJXFwdffRPU
{{orgUID}}Contact.orgUIDorg_F7N5oJv8Lvf271NM
{{callStatus}}Contact.callStatusended_by_user
{{disconnectReason}}Contact.disconnectReasonCLIENT_INITIATED
{{billingDuration}}Contact.billingDuration67
{{callDuration}}Contact.callDuration67
{{attempts}}Contact.attempts2
{{recordingUrl}}Contact.recordingUrl/recording/ETD1RtRBREAj
{{summary}}Contact.analysis.summaryUser interested in plots...
{{sentiment}}Contact.analysis.sentimentpositive
{{analysis}}Contact.analysis (full JSON){"is_interested":true,...}
{{analysis.KEY}}Any key inside analysisSee below
{{transcription}}Contact.transcription (full JSON){"items":[...]}

Analysis Dot-Notation Access ​

Every key inside the analysis object is automatically flattened with an analysis. prefix. This means if your analysis schema produces:

json
{
  "is_interested": true,
  "sentiment": "positive",
  "summary": "User wants premium plots...",
  "budget_range": "50L-1Cr",
  "follow_up_needed": true,
  "lead_score": 8
}

You can reference each field individually in your body template:

VariableResolves To
{{analysis.is_interested}}true
{{analysis.sentiment}}positive
{{analysis.summary}}User wants premium plots...
{{analysis.budget_range}}50L-1Cr
{{analysis.follow_up_needed}}true
{{analysis.lead_score}}8
{{analysis}}Full JSON string (all keys)
{{summary}}Shortcut for {{analysis.summary}}
{{sentiment}}Shortcut for {{analysis.sentiment}}

This works with any custom analysis schema whatever fields your agent's analysis produces, they're all available as {{analysis.field_name}}.

Dynamic Variables (from CSV agent template variables) ​

Any column in the uploaded CSV that matches the agent's template variables is stored in Contact.dynamicVariables and available as {{column_name}}.

Example: If agent has template variable location and CSV has a location column → {{location}} resolves to the per-contact value.

Extra CSV Columns (not in agent template variables) ​

CSV columns that don't match any agent template variable are stored in Contact.extraCsvData and are also available as {{column_name}}.

Example: CSV has columns phone, name, location, crm_id. Agent only has location as a template variable. The crm_id column is stored in extraCsvData and available as {{crm_id}} in API action body templates.

Example Body Template ​

json
{
  "phone": "{{phone}}",
  "name": "{{name}}",
  "call_status": "{{callStatus}}",
  "duration_seconds": "{{billingDuration}}",
  "summary": "{{summary}}",
  "sentiment": "{{sentiment}}",
  "interested": "{{analysis.is_interested}}",
  "lead_score": "{{analysis.lead_score}}",
  "budget": "{{analysis.budget_range}}",
  "crm_lead_id": "{{crm_id}}",
  "recording": "{{recordingUrl}}"
}

Unresolved variables (e.g., {{summary}} before analysis is ready) are replaced with empty string the request is still sent, just with blank values for unavailable fields.

Edge Case Handling ​

ScenarioBehavior
{{analysis.budget_range}} but analysis has no budget_range keyResolves to "" (empty string)
{{analysis.is_interested}} but analysis is null (not yet available)Resolves to "" the entire analysis block is skipped
{{analysis}} with analysis = nullResolves to ""
Analysis value is true / falseResolves to "true" / "false" (string)
Analysis value is a number like 8Resolves to "8" (string)
Analysis value is an array ["tag1", "tag2"]Resolves to '["tag1","tag2"]' (JSON string)
Analysis value is null or undefinedKey is skipped, resolves to ""
{{unknown_variable}} key doesn't exist anywhereResolves to ""
Contact has no dynamicVariables or extraCsvDataThose blocks are skipped, system vars still work
contact.analysis is a string (bad data)Stored as-is in {{analysis}}, no dot-notation flattening
Body template is emptyNo body sent (GET/DELETE) or empty body (POST/PUT)
Body template has invalid JSON with variablesSent as-is after interpolation the receiving API decides

The system never throws or crashes on missing data. Every {{variable}} either resolves to a value or to an empty string.


Data Flow CSV to Contact to API Action ​


Retry Schedule ​

When retryOnFailure is enabled on an API Action, failed executions are retried with exponential backoff:

AttemptDelay
15 seconds
215 seconds
31 minute
45 minutes

After all retries are exhausted, the execution is marked as failed permanently in ApiActionLog.

Default max retries: 3 (configurable per action, 0–10 range).


Execution Logging ​

Every API Action execution (event-triggered, manual test, or tool-call) is logged in the ApiActionLog collection with:

  • Request: URL, method, headers (auth headers masked), body
  • Response: HTTP status, body (first 8KB), latency
  • Context: trigger type (event / manual / tool_call), event type, contactUID, actionId
  • Result: success / failed / pending, attempt number, error message

Logs have a 30-day TTL index and are viewable at /logs under the API Action Logs tab. See TOOLS-AND-LOGS.md for the full tab layout.


MongoDB Schema ​

ApiAction ​

Collection: ApiAction
Indexes:
  - actionId (unique)
  - customerUID + deletedAt
  - customerUID + eventTypes + isActive + deletedAt
  - customerUID + isToolEnabled + isActive + deletedAt
  - campaignUID
FieldTypeDescription
actionIdstringUnique ID (act_ prefix)
customerUIDstringOwner (tenant isolation)
orgUIDstringOrganization
namestringDisplay name
descriptionstringOptional description
urlstringTarget HTTPS URL
methodenumGET, POST, PUT, PATCH, DELETE
headersarrayCustom headers [{key, value, enabled}]
bodyTypeenumjson, form, raw, none
bodyTemplatestringBody with {{variable}} placeholders
authobject{type, token, headerName, username, password} (encrypted at rest)
eventTypesstring[]Events that trigger this action
campaignUIDstring | nullCampaign scope (null = global)
timeoutMsnumberRequest timeout (1000–30000ms)
retryOnFailurebooleanEnable retries
maxRetriesnumberMax retry attempts (0–10)
sourceenummanual, curl_import, swagger_import
isActivebooleanActive toggle
isToolEnabledbooleanAvailable as voice agent tool
toolNamestring | nullTool identifier for agent
toolDescriptionstring | nullTool description for LLM
deletedAtDate | nullSoft delete timestamp

ApiActionLog ​

Collection: ApiActionLog
Indexes:
  - logId (unique)
  - customerUID + createdAt (desc)
  - actionId + createdAt (desc)
  - contactUID + createdAt (desc)
  - createdAt (TTL: 30 days)
FieldTypeDescription
logIdstringUnique ID (aal_ prefix)
actionIdstringParent action
triggerTypeenumevent, manual, tool_call
eventTypestring | nullEvent that triggered it
contactUIDstring | nullContact context
urlstringResolved URL
methodstringHTTP method
requestHeadersobject | nullSent headers (auth masked)
requestBodystring | nullSent body (first 8KB)
responseStatusnumber | nullHTTP response code
responseBodystring | nullResponse body (first 8KB)
latencyMsnumber | nullRound-trip time
statusenumpending, success, failed
attemptnumberAttempt number
errorstring | nullError message

API Endpoints ​

All require JWT + webhooks feature access.

MethodPathDescription
POST/api-actionsCreate action (with optional campaignUID)
GET/api-actionsList all actions (optional ?campaignUID= filter)
GET/api-actions/:actionIdGet single action
PUT/api-actions/:actionIdUpdate action
DELETE/api-actions/:actionIdSoft-delete action
POST/api-actions/import/curlParse cURL or raw HTTP request → action config (doesn't create)
POST/api-actions/:actionId/testTest execution with sample data (returns resolvedUrl, requestHeaders, requestBody for debugging)
GET/api-actions/template-variablesList available variables + event types
GET/api-actions/toolsList tool-enabled actions (for agent server)
GET/api-action-logsList execution logs (paginated, filtered)
GET/api-action-logs/:logIdGet single log

Security ​

  • Auth secrets encrypted at rest: Bearer tokens, API keys, and passwords are AES-256-GCM encrypted before storage in MongoDB (same encryption as webhook secrets)
  • Auth secrets masked in responses: API responses never return raw secrets only first 4 chars + ****
  • HTTPS enforced: Action URLs must use HTTPS (HTTP allowed only for localhost in development)
  • Tenant isolation: All queries scoped to customerUID users can only see/modify their own actions
  • Per-customer limits: Maximum 25 API actions per customer
  • Response body capped: Only first 8KB of response body is stored in logs
  • Request timeout: Configurable 1–30 second timeout per action

UI Flow ​

Campaign Upload (create) ​

Upload Page
  ├── CSV file drop zone + "Example CSV" download
  ├── Campaign name
  ├── Agent selection
  ├── Variable mismatch warnings (extra CSV cols noted as "available in API action payloads")
  ├── API Action toggle
  │     └── Multiple API Actions (Add Another API Action button)
  │           ├── Action 1
  │           │     ├── Import from cURL / raw HTTP request
  │           │     ├── Name + Method + URL
  │           │     ├── Custom Headers (key-value pairs with enable/disable toggle)
  │           │     ├── Body template editor (with clickable variable badges)
  │           │     ├── Auth config (None / Bearer / API Key / Basic) with show/hide toggle
  │           │     ├── Event type checkboxes
  │           │     ├── Conditional execution (per-action conditions on analysis fields)
  │           │     └── Test button (with debug info: resolved URL, headers sent, request body)
  │           ├── Action 2 (independent config, conditions, auth)
  │           └── ...
  ├── Schedule toggle
  └── Upload button → creates Campaign + multiple ApiActions in sequence

Campaign Detail (manage) ​

Campaign Detail Page
  ├── Stats cards (total, completed, failed, pending)
  ├── API Actions card
  │     ├── List of actions with method badge, URL, event badges
  │     ├── Active/inactive toggle per action
  │     ├── Delete button per action
  │     └── "View Logs" link → navigates to Integrations page
  └── Contacts table

Monitoring (/logs) ​

Historical runs are viewable on the Logs page. The tab is one of four, all built on the shared LogsExplorer component:

/logs
  ├── Webhook Logs
  ├── Client Tool Logs
  ├── Integration Logs
  └── API Action Logs        ← legacy, this system
        ├── Action ID filter
        ├── Status filter (success / failed / pending)
        ├── Trigger type filter (event / manual / tool_call)
        ├── Inclusive From / To date range
        ├── Expandable rows with request/response detail
        └── Pagination

cURL / Raw HTTP Import ​

The import endpoint (POST /api-actions/import/curl) accepts two formats:

Standard cURL command ​

bash
curl -X POST https://api.example.com/leads \
  -H "Authorization: Bearer abc123" \
  -H "Content-Type: application/json" \
  -d '{"phone": "{{phone}}", "name": "{{name}}"}'

Raw HTTP request format (RFC 7230) ​

Useful for importing from tools like Postman "Raw" export, browser DevTools "Copy as HTTP", or API docs:

POST /api/v2/whatsapp-business/messages HTTP/1.1
Host: app.convosia.com
Authorization: Bearer 9b09a31172e6d8de0b0d29ef7fa80503480a3ca110d98f4a3449e797dd4fd2b8
Content-Type: application/json

{"to": "918010545225", "phoneNoId": "785465101322330", "type": "template"}

The parser:

  • Detects format automatically (starts with METHOD /path HTTP/x.x = raw HTTP)
  • Reconstructs full URL from Host header + path (uses HTTPS by default)
  • Extracts Authorization header into structured auth config (Bearer/Basic)
  • Filters out Host, Content-Length (auto-computed)
  • Returns remaining headers as custom headers for user to review
  • Extracts body (everything after the blank line separator)

Multiple API Actions Per Campaign ​

Users can configure multiple independent API actions during campaign upload. Each action has its own:

  • URL, method, and custom headers
  • Body template with different variable usage
  • Authentication (can use different APIs with different auth)
  • Event types (Action A fires on call.ended, Action B on call.completed)
  • Conditions (Action A fires only when is_interested=true, Action B fires unconditionally)

At dispatch time, ALL matching actions are queried and processed independently. One action failing doesn't affect others.


Conditional Execution ​

Each API action can optionally have conditions that must ALL be met (AND logic) before the action fires:

json
{
  "conditions": [
    { "field": "is_interested", "operator": "eq", "value": true },
    { "field": "sentiment", "operator": "eq", "value": "positive" }
  ]
}

Supported operators: eq, neq, exists, not_exists, contains, gt, lt

Condition evaluation:

  • Fields are resolved from contact.analysis (case-insensitive lookup)
  • Boolean string comparison handles "true" matching true
  • If analysis is null/missing, only not_exists operator can match
  • Unmet conditions cause the action to be silently skipped (no error, no log entry)

File Map ​

services/api-action.service.ts        Core: template interpolation, cURL parser, BullMQ queue/worker, enqueueApiActionEvent
controllers/api-action.controller.ts  CRUD + test + logs API handlers
repositories/apiAction.repository.ts  Data access layer (ApiAction + ApiActionLog)
routes/api-action.routes.ts           Route definitions
models/ApiAction.ts                   Mongoose model for actions
models/ApiActionLog.ts                Mongoose model for execution logs (30-day TTL)
types/api-action.types.ts             TypeScript interfaces, enums, constants
utils/csv.utils.ts                    CSV parser (preserves extra columns in extraCsvData)
controllers/webhook.controller.ts     Lifecycle hooks (calls enqueueApiActionEvent alongside webhooks)
server.ts                             initApiActionWorker() / closeApiActionWorker() lifecycle
app.ts                                Route registration

Future: Voice Agent Tool Calling ​

API Actions with isToolEnabled=true can be invoked by the agent server during live calls. The agent LLM sees the action as a callable tool with toolName and toolDescription. When invoked:

  1. Agent server calls GET /api-actions/tools to discover available tools for the customer
  2. During a call, the LLM decides to call a tool → agent server calls executeApiActionDirect() with triggerType: "tool_call" and live call context
  3. The response is captured in ApiActionLog and returned to the LLM for conversation continuation

This enables real-time CRM updates during calls (e.g., "Let me check your appointment" → tool calls CRM API → agent reads result back to caller).

Last updated: