ErzyCall API
    ErzyCall API

    Getting Started

    Quick StartAuthentication

    Guides

    Rate LimitingError HandlingWebhooks

    MCP

    OverviewChat assistantsCoding toolsAutomation platformsTools & permissionsTroubleshooting

    API Reference

    OrganizationCallsContactsCasesAssistantsPhone NumbersContact GroupsUsageWebhook EndpointsWhatsApp

    WhatsApp

    Access connected WhatsApp Business accounts, send messages, and manage chat threads.

    WhatsApp APIs are only available after connecting a WhatsApp Business Account (WABA) in Settings → Integrations → WhatsApp.

    Get Connected Accounts

    List all WhatsApp Business accounts connected to your organization.

    GET /api/v1/whatsapp/accounts

    Required scope: whatsapp:read

    Example Request

    curl -X GET "https://app.erzycall.com/api/v1/whatsapp/accounts" \
      -H "X-API-Key: ek_live_abc123"

    Example Response

    {
      "data": [
        {
          "id": "wa_acct_abc123",
          "status": "active",
          "statusMessage": null,
          "wabaId": "123456789012345",
          "phoneNumberId": "987654321098765",
          "displayPhoneNumber": "+60360431879",
          "verifiedName": "My Business",
          "qualityRating": "GREEN",
          "messagesSent": 142,
          "messagesReceived": 89,
          "lastMessageAt": "2025-01-15T14:30:00.000Z",
          "createdAt": "2025-01-01T09:00:00.000Z"
        }
      ]
    }

    Account Status Values

    StatusDescription
    activeConnected and ready to send/receive messages
    disconnectedDisconnected — no messages can be sent
    errorConnection error — check statusMessage for details
    pending_setupSetup in progress

    List Conversations

    Retrieve chat threads for your organization, sorted by most recent activity.

    GET /api/v1/whatsapp/conversations

    Required scope: whatsapp:read

    Query Parameters

    ParameterTypeDefaultDescription
    limitinteger25Results per page (1–100)
    cursorstring—Pagination cursor from a previous response
    statusstring—Filter by status: active, handed_off, closed

    Example Request

    curl -X GET "https://app.erzycall.com/api/v1/whatsapp/conversations?limit=10&status=active" \
      -H "X-API-Key: ek_live_abc123"

    Example Response

    {
      "data": [
        {
          "id": "conv_abc123",
          "status": "active",
          "waUserPhone": "+60111234567",
          "waUserName": "Ahmad Razif",
          "contactId": "contact_456",
          "integrationId": "wa_acct_abc123",
          "lastMessagePreview": "Sure, I'll look into it right away.",
          "lastMessageDirection": "outbound",
          "lastUserMessageAt": "2025-01-15T14:25:00.000Z",
          "sessionExpiresAt": "2025-01-16T14:25:00.000Z",
          "messageCount": 12,
          "aiSummary": null,
          "createdAt": "2025-01-14T10:00:00.000Z",
          "updatedAt": "2025-01-15T14:25:00.000Z"
        }
      ],
      "pagination": {
        "cursor": "eyJwb3...",
        "hasMore": true,
        "pageSize": 10
      }
    }

    Conversation Status Values

    StatusDescription
    activeOngoing — bot or agent is handling the thread
    handed_offEscalated to a human agent
    closedResolved and closed

    Get Conversation

    Retrieve a single conversation including its recent message history.

    GET /api/v1/whatsapp/conversations/{id}

    Required scope: whatsapp:read

    Example Request

    curl -X GET "https://app.erzycall.com/api/v1/whatsapp/conversations/conv_abc123" \
      -H "X-API-Key: ek_live_abc123"

    Example Response

    {
      "data": {
        "id": "conv_abc123",
        "status": "active",
        "waUserPhone": "+60111234567",
        "waUserName": "Ahmad Razif",
        "contactId": "contact_456",
        "integrationId": "wa_acct_abc123",
        "lastMessagePreview": "Sure, I'll look into it right away.",
        "lastMessageDirection": "outbound",
        "lastUserMessageAt": "2025-01-15T14:25:00.000Z",
        "sessionExpiresAt": "2025-01-16T14:25:00.000Z",
        "messageCount": 12,
        "aiSummary": null,
        "createdAt": "2025-01-14T10:00:00.000Z",
        "updatedAt": "2025-01-15T14:25:00.000Z",
        "messages": [
          {
            "id": "msg_001",
            "direction": "inbound",
            "senderType": "user",
            "source": "whatsapp_customer",
            "messageType": "text",
            "text": "Hi, I'd like to know more about your plans.",
            "mediaUrl": null,
            "mediaCaption": null,
            "deliveryStatus": null,
            "timestamp": "2025-01-15T14:20:00.000Z",
            "createdAt": "2025-01-15T14:20:00.000Z"
          },
          {
            "id": "msg_002",
            "direction": "outbound",
            "senderType": "bot",
            "source": "erzycall_ai",
            "messageType": "text",
            "text": "Sure, I'll look into it right away.",
            "mediaUrl": null,
            "mediaCaption": null,
            "deliveryStatus": "read",
            "timestamp": "2025-01-15T14:25:00.000Z",
            "createdAt": "2025-01-15T14:25:00.000Z"
          }
        ]
      }
    }

    Returns up to the 50 most recent messages, in chronological order.

    senderType identifies the actor (user, bot, agent, or system) while source identifies message provenance (for example erzycall_ai, erzycall_ui, erzycall_api, whatsapp_customer, or whatsapp_coexistence). Existing rows that predate provenance return legacy_unknown.

    Errors

    StatusCodeDescription
    404NOT_FOUNDConversation not found

    Send Message

    Send a text message or approved template to an existing conversation.

    POST /api/v1/whatsapp/messages

    Required scope: whatsapp:write

    24-hour session window: WhatsApp only allows free-form text messages within 24 hours of the last inbound message. Once the window expires, you must send a template message to re-open the conversation. Check sessionExpiresAt on the conversation to see when the window closes.


    Optional external report context

    Text and supported text-template sends accept an optional context string. With MCP, pass it as message.context to send_whatsapp_message; existing confirmation and idempotency requirements still apply.

    {
      "conversationId": "conv_abc123",
      "type": "text",
      "text": "MyHappyBusiness has one document request to close today.",
      "context": "R42, 09:00 MYT; registration document missing; owner Anna; task CU-101; due 17:00."
    }

    Only the visible text or resolved approved template goes to WhatsApp. Context is stored in a private context field on the normal WhatsApp message, separate from its visible text. Ordinary UI queries, previews and API responses omit context. The next inbound turn receives one labelled assistant history entry per report, with the original reference, time, visible text and untrusted reference data. Provenance and the internal/visible distinction are explained inside that assistant entry; no external-report guidance is appended to the configured system prompt. Report arrival does not trigger an AI answer. An explicit WhatsApp reply can recover its original retained report beyond the recent history page, within the same tenant and conversation. Missing or erased context is reported as unavailable while conversation continues.

    Do not include secrets or facts that this recipient or agent may not access. “Internal” means not sent directly; relevant facts may appear in an answer. Reports are historical snapshots, not current-state proof or permission to act. Templates must be resolvable by this sender (text header/body/footer and static buttons); unsupported media or dynamic-button parameters are not guessed.

    Use the same Idempotency-Key and exact payload on retry. Changed context conflicts just like changed visible text. 201 means provider acceptance, distinct from subsequent delivery/read status. MESSAGE_PERSISTENCE_UNAVAILABLE means Meta accepted the message but normal message/context persistence failed: do not resend. DELIVERY_UNCONFIRMED means the provider outcome is unknown and requires reconciliation. There is no separate report table, lifecycle, report ID, callback reference or report-specific receipt repair. Context follows its message's existing deletion semantics. This simpler design accepts the existing send-to-persistence crash gap; it does not promise durable recovery of private context after acceptance.

    The actual Convex 1 MiB document limit applies to the whole message. Fields known before dispatch are checked (413, nothing sent); the complete record is checked again when Meta's ID is available. If that final write fails, the accepted-but-unsaved response above is returned. There is no extra context-size policy, report-count limit or age cutoff. History reads check actual transaction headroom before reading each full row, including inline context bytes. Optional history uses a nested read budget that reserves the required turn-admission write. Platform read limits or actual provider context overflow produce structured omission data while the current user request stays intact. Large REST conversation pages may return messagesTruncated: true and messagesStatus: "platform_read_limit"; UI pagination returns a continuation cursor rather than reading past the platform budget.

    Send Text Message

    Send a plain text message within an active session window.

    Request Body

    FieldTypeRequiredDescription
    typestringYesMust be "text"
    conversationIdstringYesID of the conversation to send to
    textstringYesMessage text (max 4,096 characters)

    Example Request

    curl -X POST "https://app.erzycall.com/api/v1/whatsapp/messages" \
      -H "X-API-Key: ek_live_abc123" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "text",
        "conversationId": "conv_abc123",
        "text": "Hi Ahmad, your appointment is confirmed for tomorrow at 10am."
      }'

    Example Response (201 Created)

    {
      "data": {
        "messageId": "wamid.HBgLNjAxMTIzNDU2N...",
        "type": "text"
      }
    }

    Errors

    StatusCodeDescription
    404NOT_FOUNDConversation not found or no active integration
    422SESSION_EXPIRED24-hour window expired — send a template instead
    502WHATSAPP_API_ERRORMeta API rejected the message

    Optional reply buttons

    Add buttons to a text send, or message.buttons in the existing send_whatsapp_message MCP tool. Keep the existing confirmed: true and stable idempotencyKey for MCP, or the Idempotency-Key header for REST.

    {
      "conversationId": "conv_abc123",
      "type": "text",
      "text": "There is an unpaid invoice. What would you like next?",
      "context": "Invoice INV-101. Balance remains unverified.",
      "buttons": [
        { "id": "draft_reminder", "title": "Draft reminder" },
        { "id": "show_details", "title": "Show details" }
      ]
    }

    WhatsApp places the buttons. There must be 1–3 choices, with nonblank, unique IDs (maximum 256 characters) and nonblank, unique titles (maximum 20 characters). The visible text must be nonblank and at most 1,024 characters when buttons are present. These limits count Unicode code points, preserving supplied content. Plain text without buttons keeps its existing 4,096 UTF-16-unit limit. Invalid choices return 400 VALIDATION_ERROR with field details before any send attempt. Custom buttons on templates, URL/call buttons, lists, and extra button properties are rejected. There is no truncation or automatic fallback to text/templates.

    Buttons require the same verified open 24-hour window as text. Closed windows return 422 SESSION_EXPIRED; use an eligible approved template separately. A successful button send returns 201 with data.type: "interactive", meaning Meta accepted it. Delivery/read status remains separate. Idempotency binds the entire button payload, including order, IDs and titles. Unknown or accepted-but-unsaved outcomes must be reconciled; never resend automatically.

    Only visible text and buttons go to Meta. Optional context remains private on the ordinary outbound message. A tap becomes a normal user turn with its selected ID/title and the retained original in assistant history, including originals outside the recent history page and originals without private context. Missing or mismatched originals produce an honest clarification while chat continues. Buttons create no Request/Bridge entity, execute no tool themselves, and grant no action authority. Repeated taps do not grant permission to repeat irreversible actions.

    Send Template Message

    Send a pre-approved WhatsApp message template. Templates can be sent at any time — they are the only way to initiate or re-open a conversation after the 24-hour window closes.

    Templates must be APPROVED in your WhatsApp Business Manager before they can be sent. You can sync and view your templates in Settings → Integrations → WhatsApp → Templates.

    Request Body

    FieldTypeRequiredDescription
    typestringYesMust be "template"
    conversationIdstringYesID of the conversation to send to
    templateNamestringYesExact template name as registered in Meta Business Manager
    templateLanguagestringYesLanguage code (e.g., en_US, ms, ar)
    variablesobjectNoKey-value pairs to fill template placeholders (e.g., {"name": "Ahmad", "date": "Jan 16"})

    Example Request

    curl -X POST "https://app.erzycall.com/api/v1/whatsapp/messages" \
      -H "X-API-Key: ek_live_abc123" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "template",
        "conversationId": "conv_abc123",
        "templateName": "appointment_reminder",
        "templateLanguage": "en_US",
        "variables": {
          "name": "Ahmad",
          "date": "January 16",
          "time": "10:00 AM"
        }
      }'

    Example Response (201 Created)

    {
      "data": {
        "messageId": "wamid.HBgLNjAxMTIzNDU2N...",
        "type": "template",
        "template": {
          "name": "appointment_reminder",
          "language": "en_US"
        }
      }
    }

    Errors

    StatusCodeDescription
    404NOT_FOUNDConversation not found or no active integration
    404TEMPLATE_NOT_FOUNDTemplate not found or status is not APPROVED
    502WHATSAPP_API_ERRORMeta API rejected the message (e.g., template parameter mismatch)

    Template variables: Variable names must match the parameter names defined in your template. For templates using positional parameters ({{1}}, {{2}}), use "1", "2" as the keys in the variables object.

    Template catalog management

    The public API supports the complete basic lifecycle for tenant-owned WhatsApp templates. First call GET /whatsapp/accounts and select the exact account id; create and sync never choose the first account implicitly.

    Method and routeScopePurpose
    GET /api/v1/whatsapp/templateswhatsapp_templates:readList local catalog summaries
    GET /api/v1/whatsapp/templates/{templateId}whatsapp_templates:readRead safe components, samples, status, and freshness
    POST /api/v1/whatsapp/templateswhatsapp_templates:writeCreate and submit to Meta
    PATCH /api/v1/whatsapp/templates/{templateId}whatsapp_templates:writeUpdate supported authoring fields
    POST /api/v1/whatsapp/templates/syncwhatsapp_templates:writeReconcile one account with Meta
    DELETE /api/v1/whatsapp/templates/{templateId}whatsapp_templates:writeDelete one exact language version

    templateId/id is the local ErzyCall ID. metaTemplateId is the read-only provider ID. GET requests read the local catalog and never trigger a hidden sync. Use lastSyncedAt to judge freshness.

    List accepts integrationId, status, category, language, exact name, name substring search, limit (1–100, default 25), and cursor. Filters are combined and results are ordered by newest local creation first. Keep all filters unchanged while following a cursor; a foreign, malformed, or filter-mismatched cursor returns 400 INVALID_CURSOR without disclosing another tenant's data.

    Create with review samples

    Create requires an Idempotency-Key header of 8–200 characters. V1 authoring supports MARKETING/UTILITY BODY, optional FOOTER, and QUICK_REPLY/URL/PHONE_NUMBER buttons. AUTHENTICATION, media headers, carousel, and OTP shapes can be read/synced/deleted but return 422 UNSUPPORTED_TEMPLATE_SHAPE for authoring.

    curl -X POST "https://app.erzycall.com/api/v1/whatsapp/templates" \
      -H "X-API-Key: ek_live_abc123" \
      -H "Idempotency-Key: account-update-2026-10-03" \
      -H "Content-Type: application/json" \
      -d '{
        "integrationId": "<id from GET /whatsapp/accounts>",
        "name": "account_update",
        "language": "en_US",
        "category": "UTILITY",
        "body": "Hello {{first_name}}, your account update is ready to review in your customer portal. Please open the portal for the latest details and contact our team if you need help.",
        "sampleValues": { "first_name": "Alex" }
      }'

    For positional authoring, use {{1}} and "sampleValues": { "1": "Alex" }. These are fictional review samples sent with the template definition; they are not the variables supplied when sending a real message.

    ErzyCall's current authoring policy requires at least 15 useful static words per variable, sequential positional variables, lowercase named variables, required samples, and plain punctuation. These local checks reduce avoidable rejection but do not guarantee Meta approval.

    Read, sync, and moderation status

    curl "https://app.erzycall.com/api/v1/whatsapp/templates?integrationId=<account-id>&status=REJECTED&limit=25" \
      -H "X-API-Key: ek_live_abc123"
    
    curl "https://app.erzycall.com/api/v1/whatsapp/templates/<template-id>" \
      -H "X-API-Key: ek_live_abc123"
    
    curl -X POST "https://app.erzycall.com/api/v1/whatsapp/templates/sync" \
      -H "X-API-Key: ek_live_abc123" \
      -H "Content-Type: application/json" \
      -d '{ "integrationId": "<account-id>" }'

    Sync returns { integrationId, synced, created, updated, deleted, lastSyncedAt }. It follows every provider page before stale cleanup. A later-page error or malformed/unsupported provider record fails the sync without stale cleanup or a false freshness update. Statuses are APPROVED, PENDING, REJECTED, PAUSED, and DISABLED; rejectedReason is cleared after the template leaves REJECTED.

    A successful HTTP submission means Meta accepted the management request, not that moderation approved it. Sync, then GET detail to observe asynchronous rejection or the authoritative status.

    Edit and exact delete

    PATCH accepts any nonempty combination of body, footer, buttons, sampleValues, and category. Omitted fields remain unchanged. footer: "" and buttons: [] explicitly clear them. name, language, integrationId, and metaTemplateId are immutable. V1 edits only APPROVED/REJECTED templates; APPROVED category changes are rejected. Templates with headers, carousel, OTP, or unknown components return 422 rather than losing data. Provider edit-frequency limits remain authoritative; successful edit submission does not imply or fabricate PENDING.

    curl -X PATCH "https://app.erzycall.com/api/v1/whatsapp/templates/<template-id>" \
      -H "X-API-Key: ek_live_abc123" \
      -H "Idempotency-Key: revise-account-update-v2" \
      -H "Content-Type: application/json" \
      -d '{ "footer": "Questions? Contact support." }'
    
    curl -X DELETE "https://app.erzycall.com/api/v1/whatsapp/templates/<template-id>" \
      -H "X-API-Key: ek_live_abc123" \
      -H "Idempotency-Key: delete-account-update-en-us"

    Delete uses the template's own account and exact name + metaTemplateId, so it removes one language version. 204 means the exact provider request (or exact confirmed absence) and local deletion completed. A generic provider error never deletes the local record. 202 means Meta succeeded but local reconciliation is required. Meta does not allow deleting DISABLED templates; a deleted approved name can remain unavailable for 30 days, and incomplete delivery can lead to provider PENDING_DELETION.

    Retries, partial success, and errors

    Reuse the exact same key and payload after a lost response. Reusing a key with another payload returns 409 IDEMPOTENCY_MISMATCH. Create/edit/delete reserve the operation before contacting Meta, preventing concurrent duplicate submissions. A confirmed response is replayed for 24 hours. A provider timeout has WHATSAPP_OUTCOME_UNKNOWN; sync/read before deciding whether to start a new operation because Graph API does not guarantee exactly-once writes.

    If Meta succeeds but local persistence fails, the API returns 202 with reconciliationRequired: true, metaTemplateId, and id: null when no local ID exists. For delete it also returns remoteDeleted: true. Run sync, then list/detail by name and language.

    Provider submission failures use a stable REST code and an allowlisted error.provider object (Meta code/title/details/hint/trace ID). Async moderation rejection is a successful GET with status: "REJECTED", not an HTTP read error. Request/audit logs record action, account/resource, result, and changed field names—not body text, samples, credentials, or raw provider payloads.

    Existing whatsapp:read/whatsapp:write permissions do not grant template access. OAuth grants and refresh tokens keep only scopes already approved; reauthorize for the dedicated template scopes. API-key full access (*) retains its existing meaning.

    Agent configuration

    These endpoints manage the same tenant WhatsApp text agents used by the dashboard. Maya and WhatsApp voice agents use separate systems.

    Method and routeScopePurpose
    GET /api/v1/whatsapp/agentswhatsapp_agents:readList agent summaries
    GET /api/v1/whatsapp/agents/{agentId}whatsapp_agents:readRead stored settings, defaults, and revision
    PATCH /api/v1/whatsapp/agents/{agentId}whatsapp_agents:writeUpdate selected settings

    List accepts optional integrationId, limit (default 25, maximum 100), and cursor. Results include disabled agents and identify each account by phone/name. Duplicate names require explicit selection by agentId. Cursors are bound to the organization and account filter; restart pagination when changing filters.

    Get returns { data: { agentId, integrationId, name, accountDisplayName, accountPhoneNumber, enabled, isDefault, revision, settings, effectiveSettings } }. settings includes only persisted editable fields. effectiveSettings includes runtime defaults; do not copy those defaults into unrelated edits. The default model is claude-sonnet-5. effectiveSettings.maxTokens reports the actual per-request limit: Sonnet 5 sets this limit to at least 2048 tokens, shared by thinking and the response, even when a lower value is stored. This is a ceiling per model request, not a minimum consumption or a total conversation budget. Haiku 4.5 uses the stored limit or 1024 when absent. effectiveSettings.temperature is null for models such as Sonnet 5 that omit temperature; Haiku uses the stored value or 0.7. Stored values remain in settings and are retained when switching models.

    PATCH requires an Idempotency-Key header (8–200 characters):

    {
      "updates": { "welcomeMessage": "Welcome! How can we help?" },
      "expectedRevision": "<revision returned by GET>"
    }

    The REST body contains only updates and expectedRevision. MCP additionally requires confirmed: true and idempotencyKey. Updates return the persisted configuration, new revision, and changedFields. Omitted fields remain unchanged, including legacy settings. Empty strings clear optional text; empty arrays clear the corresponding list.

    Editable agent settings

    FieldsValidation
    nameNonblank, maximum 200 characters
    enabled, handoffEnabled, autoCreateContact, ignoreAllNewWhatsAppChatsBooleans
    autoMarkMessagesReadBoolean; defaults to true when unset. Set to false to skip ErzyCall's Meta read receipt and coupled typing indicator while AI processing and replies continue. This does not guarantee notifications on every linked client.
    systemPromptNonblank, maximum 32,000 characters
    knowledgeBasePlain text, maximum 64,000 characters
    modelclaude-sonnet-5 (default), claude-haiku-5-5, claude-haiku-4-5, gpt-6-luna, or gpt-6.1-sol; existing stored legacy models remain in use until changed
    temperatureNumber from 0 to 1; retained but not sent for Sonnet 5, Haiku 5.5, or GPT-6.1 Sol
    maxTokensInteger from 50 to 4096; Sonnet 5, Haiku 5.5, and GPT-6.1 Sol apply a minimum effective limit of 2048 per request, reported in effectiveSettings.maxTokens
    welcomeMessage, fallbackMessage, handoffMessageMaximum 4096 characters each; empty string clears
    handoffKeywordsUp to 100 strings, maximum 200 characters each
    audienceGroupIdsUp to 100 tenant contact-group IDs; deduplicated; a group may route to only one agent per account
    allowedToolsDeduplicated list from the shared capability registry
    externalToolIdsComplete deduplicated list of tenant tool IDs; empty array removes all external assignments

    Supported capabilities: get_contact_profile, get_organization_contact_count, get_recent_whatsapp_questions, get_contact_group_directory. Recent questions remain restricted to the current conversation. Discover group IDs with list_contact_groups. Agent IDs, organization, integration, default status, timestamps, and greeting provenance are read-only.

    The full JSON request must fit within 128 KiB of UTF-8, even if individual fields meet their character limits. Unknown fields, nulls, empty updates, and invalid values are rejected without writing. Changed greetings become custom; unchanged autogenerated greetings retain their provenance.

    Consent, retries, and errors

    Existing WhatsApp scopes cover accounts/conversations and message sending. Agent configuration requires fresh consent for the dedicated agent scopes. Refreshing an old token does not add scopes.

    Status/codeRecovery
    400 VALIDATION_ERROR / INVALID_IDCorrect the request or use a discovered ID
    400 INVALID_CURSORRestart pagination with unchanged filters
    401Sign in again or replace expired/revoked credentials
    403 INSUFFICIENT_SCOPEReauthorize with the required agent scopes
    404 NOT_FOUNDSelect a resource in the connected organization
    409 REVISION_CONFLICTRead current values, review and confirm a new edit with a new key
    409 IDEMPOTENCY_MISMATCHA key was reused for a different payload; use the original payload for a retry
    409 AUDIENCE_CONFLICTSelect groups not assigned to another agent in this account
    413 PAYLOAD_TOO_LARGEReduce the request below 128 KiB
    429Honor Retry-After

    Reuse the exact key, revision, and payload for retries. For 24 hours, a committed edit replays its original successful response, including after a lost response or a later edit. The configuration patch and retry receipt are saved atomically. The revision detects dashboard changes too.

    Read back after saving. Changes apply when the runtime next loads the agent; an already-running reply may finish using its earlier snapshot. Saving or enabling an agent sends no message, reconnects no account, and replays no past messages.

    Agent tool library and explicit HTTP tests

    The public API uses the same tenant tool library, agent assignments, runtime, and execution history as the dashboard.

    Method and routeScopePurpose
    GET /api/v1/whatsapp/toolswhatsapp_agents:readList the tenant tool library
    GET /api/v1/whatsapp/tools/{toolId}whatsapp_agents:readRead one secret-safe tool summary
    GET /api/v1/whatsapp/tools/sourceswhatsapp_agents:readList compatible platform and connected-app sources
    POST /api/v1/whatsapp/toolswhatsapp_agents:writeCreate one tool
    PATCH /api/v1/whatsapp/tools/{toolId}whatsapp_agents:writePartially update one saved tool
    DELETE /api/v1/whatsapp/tools/{toolId}whatsapp_agents:writeDelete a tool and remove it from assigned agents
    POST /api/v1/whatsapp/tools/testwhatsapp_agents:writeMake one explicit request from an unsaved HTTP draft
    POST /api/v1/whatsapp/tools/{toolId}/testwhatsapp_agents:writeMake one explicit request using a saved HTTP tool
    GET /api/v1/whatsapp/tool-executions?conversationId={conversationId}whatsapp:readList execution metadata and pagination cursors
    GET /api/v1/whatsapp/tool-executions/{executionId}whatsapp:readRead the complete stored input, result, and status

    Library reads identify the stored kind honestly as http, mcp, platform, or composio. HTTP tests support http only. List and detail never return stored header values. headerNames lists names only, hasCredentials reports whether any exist, and urlPreview retains the origin while replacing path and query material with [REDACTED] markers.

    Library writes require confirmed: true and an Idempotency-Key of 8–200 characters. Create requires complete configuration. PATCH is partial: omitted fields remain unchanged; supplied inputSchema and httpParameters replace those nested objects; null clears an optional field only when the resulting complete configuration is valid. A matching retry replays the atomic transaction receipt for 24 hours, while a different payload under the same key returns 409 IDEMPOTENCY_MISMATCH. The retry fingerprint is a keyed HMAC over canonical request data, so stored receipts do not expose a plain guessable hash of credentials.

    headers is write-only and is accepted only for HTTP and manually configured MCP tools. Header names are case-insensitively unique; values reject CRLF/control characters. Accept is supported as an end-to-end response preference. Routing and transport-managed headers—including Host, Content-Length, Content-Type, Accept-Encoding, Connection, proxy headers, and Sec-*—are rejected, while names, values, count, and total bytes remain bounded. Put Content-Type: application/json on the ErzyCall REST request as shown below, not inside tool.headers. Platform and connected-app (composio) tools reject headers and named retention while allowing ordinary non-credential edits. OAuth-managed MCP tools remain fully connection-managed and reject every library edit.

    Legacy dashboard imports may contain transport-managed stored header names. Reads and tests omit those unusable names, and the next successful save removes them without exposing their values. Supported headers such as Accept remain available for same-origin edits and tests.

    PATCH credential semantics are explicit:

    • Omit both headers and preserveHeaderNames to retain all stored headers only when scheme, host, and port are unchanged.
    • Supply headers to replace every unselected stored header; {} clears all. Combine it with preserveHeaderNames to retain selected old values while adding or rotating others. A supplied header wins on a case-insensitive name match.
    • Supply preserveHeaderNames without headers to retain only those names; matching is case-insensitive and duplicate requested names are rejected.
    • Changing origin drops all old headers and returns credentialStatus.credentialsDropped: true. Cross-origin preservation is rejected. New explicit headers may be supplied for the new origin.

    If a requested preserved name is not stored, PATCH returns 400 INVALID_ARGUMENT; 404 NOT_FOUND is reserved for an unknown or foreign tool ID. Validation failures return bounded field/message details, never header values, endpoint credentials, request payloads, or stack traces.

    Manage and assign tools

    Use whatsapp_agents:read for list, detail, and sources. Create, replace, delete, assign, and test require whatsapp_agents:write. API keys use X-API-Key; OAuth clients use Authorization: Bearer <token>. Never place either credential in the JSON body.

    FieldApplies toContract
    nameAll kindsUnique local alias, 1–64 characters, beginning with a letter and containing only letters, numbers, _, or -
    descriptionAll kindsOperator-facing purpose of the tool
    kindAll kindshttp, mcp, platform, or composio
    inputSchemaAll kindsJSON Schema object for the arguments
    requiresConfirmationAll kindsWhether runtime customer confirmation is required before execution
    url, methodhttpPublic HTTPS endpoint and GET, POST, PUT, PATCH, or DELETE
    url, transport, remoteToolNamemcpPublic HTTPS endpoint, http or sse transport, and discovered remote tool name
    headersHTTP/manual MCPWrite-only authentication headers; values never appear in responses, errors, receipts, or audit bodies
    preserveHeaderNamesPATCH onlyTop-level case-insensitive selection of stored header values to retain
    platformToolIdplatformID returned by the sources endpoint
    composioToolIdcomposioConnected-app action ID returned by the sources endpoint

    List the secret-safe library:

    curl -X GET "https://app.erzycall.com/api/v1/whatsapp/tools" \
      -H "X-API-Key: ek_live_abc123"

    Create a synthetic HTTP lookup with a placeholder write-only credential:

    curl -X POST "https://app.erzycall.com/api/v1/whatsapp/tools" \
      -H "X-API-Key: ek_live_abc123" \
      -H "Idempotency-Key: create-order-lookup-01" \
      -H "Content-Type: application/json" \
      -d '{
        "confirmed": true,
        "tool": {
          "name": "lookup_order",
          "description": "Look up a synthetic order by ID",
          "kind": "http",
          "url": "https://api.example.com/orders/lookup",
          "method": "GET",
          "headers": { "Authorization": "Bearer <api-token>" },
          "inputSchema": {
            "type": "object",
            "properties": { "orderId": { "type": "string" } },
            "required": ["orderId"],
            "additionalProperties": false
          },
          "requiresConfirmation": false
        }
      }'

    The response exposes names and drop status only:

    {
      "data": {
        "id": "<toolId>",
        "credentialStatus": {
          "headerNames": ["Authorization"],
          "credentialsDropped": false
        }
      }
    }

    Partially update a description, rotate one credential, and retain another stored value by name:

    curl -X PATCH "https://app.erzycall.com/api/v1/whatsapp/tools/<tool-id>" \
      -H "X-API-Key: ek_live_abc123" \
      -H "Idempotency-Key: rotate-order-lookup-02" \
      -H "Content-Type: application/json" \
      -d '{
        "confirmed": true,
        "tool": {
          "description": "Look up a synthetic order by stable ID",
          "headers": { "Authorization": "Bearer <replacement-api-token>" }
        },
        "preserveHeaderNames": ["X-Tenant-Id"]
      }'

    Use "headers": {} to clear all stored headers. To delete the tool, send { "confirmed": true } to DELETE /whatsapp/tools/{toolId}. Deletion also removes the ID from assigned agents.

    Example value-free validation response:

    {
      "error": {
        "code": "VALIDATION_ERROR",
        "message": "Invalid tool configuration or confirmation",
        "details": [
          {
            "field": "tool.headers",
            "message": "Transport header is managed by ErzyCall and cannot be configured: Host"
          }
        ]
      }
    }

    Assign tools through the existing agent API with the complete desired ID list:

    curl -X PATCH "https://app.erzycall.com/api/v1/whatsapp/agents/<agent-id>" \
      -H "X-API-Key: ek_live_abc123" \
      -H "Idempotency-Key: assign-agent-tools-01" \
      -H "Content-Type: application/json" \
      -d '{
        "updates": { "externalToolIds": ["<toolId>"] },
        "expectedRevision": "<agent revision from GET>"
      }'

    Saving, editing, importing, or assigning a tool never calls its endpoint and never sends a WhatsApp message.

    Execution history begins when tracking is deployed; older calls have no records. List results are newest first and contain metadata only. Follow continueCursor until isDone, then use the detail route for the complete stored input and result. Endpoint credentials are never included in execution records.

    Test an unsaved or saved HTTP tool

    A draft test can supply write-only headers without saving them:

    curl -X POST "https://app.erzycall.com/api/v1/whatsapp/tools/test" \
      -H "X-API-Key: ek_live_abc123" \
      -H "Idempotency-Key: draft-order-test-01" \
      -H "Content-Type: application/json" \
      -d '{
        "tool": {
          "kind": "http",
          "url": "https://api.example.com/orders/lookup",
          "method": "POST",
          "headers": { "Authorization": "Bearer write-only-value" },
          "inputSchema": {
            "type": "object",
            "properties": { "orderId": { "type": "string" } },
            "required": ["orderId"]
          }
        },
        "arguments": { "orderId": "synthetic-order-123" },
        "confirmSideEffects": true
      }'

    A saved test supplies only arguments and intent:

    curl -X POST "https://app.erzycall.com/api/v1/whatsapp/tools/<tool-id>/test" \
      -H "X-API-Key: ek_live_abc123" \
      -H "Idempotency-Key: saved-order-test-01" \
      -H "Content-Type: application/json" \
      -d '{
        "arguments": { "orderId": "synthetic-order-123" },
        "confirmSideEffects": true
      }'
    {
      "data": {
        "status": 200,
        "ok": true,
        "elapsedMs": 87,
        "sanitizedResponse": "{\n  \"found\": true\n}",
        "truncated": false
      }
    }

    Arguments must be a JSON object matching the tool input schema. GET tests add them as query parameters. POST, PUT, PATCH, and DELETE send JSON and require confirmSideEffects: true because the request can create, update, or delete real remote data. No test is triggered automatically and the server does not automatically retry after dispatch.

    The response contains the remote status when available, ok, elapsedMs, a bounded sanitizedResponse or safe error, and truncated. Stored and supplied credential values plus common credential-bearing response fields are redacted. Saved credentials are resolved only inside the authenticated server action and are never returned or included in audit bodies.

    Every test requires a stable Idempotency-Key. Concurrent attempts are fenced and completed results replay for 24 hours. Reusing the key for a changed request returns 409 IDEMPOTENCY_MISMATCH. If a dispatched attempt loses its outcome, the key returns 409 IDEMPOTENCY_OUTCOME_UNKNOWN instead of contacting the endpoint again.

    Tests use the production server transport: public HTTPS only, TLS validation, DNS and private/reserved-address checks, redirect credential isolation, a 15-second timeout, object-schema validation, and a 32,000-character response preview. A timeout or network error can still mean the remote action ran; the remote service remains responsible for authorization and its own durable business idempotency.

    Webhook Endpoints

    Create, update, and manage webhook endpoint subscriptions.

    On this page

    Get Connected AccountsExample RequestExample ResponseAccount Status ValuesList ConversationsQuery ParametersExample RequestExample ResponseConversation Status ValuesGet ConversationExample RequestExample ResponseErrorsSend MessageOptional external report contextSend Text MessageRequest BodyExample RequestExample Response (201 Created)ErrorsOptional reply buttonsSend Template MessageRequest BodyExample RequestExample Response (201 Created)ErrorsTemplate catalog managementCreate with review samplesRead, sync, and moderation statusEdit and exact deleteRetries, partial success, and errorsAgent configurationEditable agent settingsConsent, retries, and errorsAgent tool library and explicit HTTP testsManage and assign toolsTest an unsaved or saved HTTP tool