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
| Feature | Webhooks | API Actions |
|---|---|---|
| Direction | Oktivo → Customer's receiver | Oktivo → Customer's API |
| Payload | Fixed schema (versioned) | Custom body template with {{variables}} |
| Scope | Global (all campaigns) | Campaign-specific |
| Auth | HMAC-SHA256 signature | Bearer / API Key / Basic Auth |
| Configuration | Separate Integrations page | Inline 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
| Event | Fires When | Typical Use |
|---|---|---|
call.ended | Call disconnects | Update CRM call status |
call.transcription_ready | Transcription stored on Contact | Push conversation text to CRM |
call.analysis_ready | Analysis stored on Contact | Update lead score / sentiment |
call.recording_ready | Recording URL available | Store recording link in CRM |
call.completed | All 4 above are present | Full 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)
| Variable | Source | Example Value |
|---|---|---|
{{phone}} | Contact.phone | +919876543210 |
{{name}} | Contact.name | Rahul Sharma |
{{contactUID}} | Contact.contactUID | ETD1RtRBREAj |
{{campaignUID}} | Contact.campaignUID | D-NzcJnBLqnx |
{{agentId}} | Contact.AgentID | WtJXFwdffRPU |
{{orgUID}} | Contact.orgUID | org_F7N5oJv8Lvf271NM |
{{callStatus}} | Contact.callStatus | ended_by_user |
{{disconnectReason}} | Contact.disconnectReason | CLIENT_INITIATED |
{{billingDuration}} | Contact.billingDuration | 67 |
{{callDuration}} | Contact.callDuration | 67 |
{{attempts}} | Contact.attempts | 2 |
{{recordingUrl}} | Contact.recordingUrl | /recording/ETD1RtRBREAj |
{{summary}} | Contact.analysis.summary | User interested in plots... |
{{sentiment}} | Contact.analysis.sentiment | positive |
{{analysis}} | Contact.analysis (full JSON) | {"is_interested":true,...} |
{{analysis.KEY}} | Any key inside analysis | See 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:
{
"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:
| Variable | Resolves 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
{
"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
| Scenario | Behavior |
|---|---|
{{analysis.budget_range}} but analysis has no budget_range key | Resolves to "" (empty string) |
{{analysis.is_interested}} but analysis is null (not yet available) | Resolves to "" the entire analysis block is skipped |
{{analysis}} with analysis = null | Resolves to "" |
Analysis value is true / false | Resolves to "true" / "false" (string) |
Analysis value is a number like 8 | Resolves to "8" (string) |
Analysis value is an array ["tag1", "tag2"] | Resolves to '["tag1","tag2"]' (JSON string) |
Analysis value is null or undefined | Key is skipped, resolves to "" |
{{unknown_variable}} key doesn't exist anywhere | Resolves to "" |
Contact has no dynamicVariables or extraCsvData | Those 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 empty | No body sent (GET/DELETE) or empty body (POST/PUT) |
| Body template has invalid JSON with variables | Sent 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:
| Attempt | Delay |
|---|---|
| 1 | 5 seconds |
| 2 | 15 seconds |
| 3 | 1 minute |
| 4 | 5 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| Field | Type | Description |
|---|---|---|
actionId | string | Unique ID (act_ prefix) |
customerUID | string | Owner (tenant isolation) |
orgUID | string | Organization |
name | string | Display name |
description | string | Optional description |
url | string | Target HTTPS URL |
method | enum | GET, POST, PUT, PATCH, DELETE |
headers | array | Custom headers [{key, value, enabled}] |
bodyType | enum | json, form, raw, none |
bodyTemplate | string | Body with {{variable}} placeholders |
auth | object | {type, token, headerName, username, password} (encrypted at rest) |
eventTypes | string[] | Events that trigger this action |
campaignUID | string | null | Campaign scope (null = global) |
timeoutMs | number | Request timeout (1000–30000ms) |
retryOnFailure | boolean | Enable retries |
maxRetries | number | Max retry attempts (0–10) |
source | enum | manual, curl_import, swagger_import |
isActive | boolean | Active toggle |
isToolEnabled | boolean | Available as voice agent tool |
toolName | string | null | Tool identifier for agent |
toolDescription | string | null | Tool description for LLM |
deletedAt | Date | null | Soft delete timestamp |
ApiActionLog
Collection: ApiActionLog
Indexes:
- logId (unique)
- customerUID + createdAt (desc)
- actionId + createdAt (desc)
- contactUID + createdAt (desc)
- createdAt (TTL: 30 days)| Field | Type | Description |
|---|---|---|
logId | string | Unique ID (aal_ prefix) |
actionId | string | Parent action |
triggerType | enum | event, manual, tool_call |
eventType | string | null | Event that triggered it |
contactUID | string | null | Contact context |
url | string | Resolved URL |
method | string | HTTP method |
requestHeaders | object | null | Sent headers (auth masked) |
requestBody | string | null | Sent body (first 8KB) |
responseStatus | number | null | HTTP response code |
responseBody | string | null | Response body (first 8KB) |
latencyMs | number | null | Round-trip time |
status | enum | pending, success, failed |
attempt | number | Attempt number |
error | string | null | Error message |
API Endpoints
All require JWT + webhooks feature access.
| Method | Path | Description |
|---|---|---|
POST | /api-actions | Create action (with optional campaignUID) |
GET | /api-actions | List all actions (optional ?campaignUID= filter) |
GET | /api-actions/:actionId | Get single action |
PUT | /api-actions/:actionId | Update action |
DELETE | /api-actions/:actionId | Soft-delete action |
POST | /api-actions/import/curl | Parse cURL or raw HTTP request → action config (doesn't create) |
POST | /api-actions/:actionId/test | Test execution with sample data (returns resolvedUrl, requestHeaders, requestBody for debugging) |
GET | /api-actions/template-variables | List available variables + event types |
GET | /api-actions/tools | List tool-enabled actions (for agent server) |
GET | /api-action-logs | List execution logs (paginated, filtered) |
GET | /api-action-logs/:logId | Get 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
localhostin development) - Tenant isolation: All queries scoped to
customerUIDusers 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 sequenceCampaign 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 tableMonitoring (/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
└── PaginationcURL / Raw HTTP Import
The import endpoint (POST /api-actions/import/curl) accepts two formats:
Standard cURL command
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
Hostheader + path (uses HTTPS by default) - Extracts
Authorizationheader 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 oncall.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:
{
"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"matchingtrue - If analysis is null/missing, only
not_existsoperator 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 registrationFuture: 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:
- Agent server calls
GET /api-actions/toolsto discover available tools for the customer - During a call, the LLM decides to call a tool → agent server calls
executeApiActionDirect()withtriggerType: "tool_call"and live call context - 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).