> ## Documentation Index
> Fetch the complete documentation index at: https://hellotars.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Send a campaign message

> Send one webhook campaign message to a recipient, charged per call against the campaign wallet.

Authenticate with a key generated on this campaign's **API Keys** tab, not an organization key. Run [Validate a campaign call](/docs/developer/api/webhook-campaigns/validate) first to check a request without paying for it. See the [Webhook campaigns API overview](/docs/developer/api/webhook-campaigns) for keys, contract locking, and lifecycle.

<ParamField path="campaignId" type="string" required>
  The webhook campaign ID. The **Contract** tab on the campaign page shows the full URL for your campaign.
</ParamField>

<ParamField body="to" type="string" required>
  Recipient phone number in international format with the leading `+` and country code, for example `+15551234567`. Any other format fails with `422 invalid_params` and an `errors` entry of `{ "field": "to", "code": "invalid_e164" }`. The recipient is matched to an existing end user by phone, with or without the leading `+`, before a new end user is created.
</ParamField>

<ParamField body="params" type="object">
  Template parameters as string values, keyed by parameter name. Required when the campaign's template has parameters.
</ParamField>

<ParamField body="media" type="object">
  `{ "url": "…" }`. Required when the template has a media header. Tars fetches the URL and checks it against the header's media type.
</ParamField>

## Response

<ResponseField name="status" type="string">
  `"sent"` means WhatsApp accepted the message, not that it was delivered. Delivery confirmations arrive from the provider later, and some outcomes can stay unconfirmed.
</ResponseField>

<ResponseField name="quoted_usd" type="number">
  The cost charged for this call, in USD. It is `0` when the WhatsApp number is in **Staging** mode.
</ResponseField>

<ResponseField name="balance" type="number">
  The campaign wallet balance after the charge.
</ResponseField>

<ResponseField name="tars_request_id" type="string">
  The `tars_…` request ID, for support and log correlation.
</ResponseField>

<ResponseField name="external_message_id" type="string">
  The provider's message ID for the accepted message.
</ResponseField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST \
    https://us.api.hellotars.com/api/campaigns/YOUR_CAMPAIGN_ID/send \
    -H 'Authorization: Bearer YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{ "to": "+15551234567", "params": { "order_id": "A-1042" } }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    "https://us.api.hellotars.com/api/campaigns/YOUR_CAMPAIGN_ID/send",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.TARS_API_KEY}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        to: "+15551234567",
        params: { order_id: "A-1042" }
      })
    }
  );

  const data = await response.json();
  ```

  ```python Python theme={null}
  import os
  import requests

  response = requests.post(
      "https://us.api.hellotars.com/api/campaigns/YOUR_CAMPAIGN_ID/send",
      headers={
          "Authorization": f"Bearer {os.environ['TARS_API_KEY']}",
          "Content-Type": "application/json",
      },
      json={"to": "+15551234567", "params": {"order_id": "A-1042"}},
  )

  data = response.json()
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "status": "sent",
    "quoted_usd": 0.0025,
    "balance": 48.75,
    "tars_request_id": "tars_a1b2c3d4e5f6a7b8",
    "external_message_id": "PROVIDER_MESSAGE_ID"
  }
  ```
</ResponseExample>

## Errors

Two campaign-status codes matter most for client logic.

<ResponseField name="409 campaign_paused" type="retryable">
  The campaign is not active. Retryable, because the campaign can become active again. This response carries no `Retry-After` header, so resume the campaign and retry on your own schedule.
</ResponseField>

<ResponseField name="410 campaign_gone" type="terminal">
  The campaign is cancelled or failed. Terminal, so stop calling the endpoint for this campaign.
</ResponseField>

All failures use RFC 9457 problem details with a `retryable` flag. Branch on `retryable`, not on the status number. See the [full error catalog](/docs/developer/api/webhook-campaigns#errors-rfc-9457-problem-details).


## OpenAPI

````yaml api-reference/openapi.json POST /api/campaigns/{campaignId}/send
openapi: 3.1.0
info:
  title: Tars API
  version: 3.0.0
  description: >-
    Public REST API for Tars agents: start conversations at trigger gambits,
    send channel messages, manage end users, and drive webhook campaigns.
servers:
  - url: https://us.api.hellotars.com
    description: United States
  - url: https://eu.api.hellotars.com
    description: European Union
  - url: https://in.api.hellotars.com
    description: India
  - url: https://qa.api.hellotars.com
    description: Qatar
security:
  - bearerAuth: []
tags:
  - name: Triggers
    description: Start conversations at trigger gambits
  - name: Channels
    description: Outbound channel message sends
  - name: End users
    description: End-user record management
  - name: Campaigns
    description: Webhook campaign sends and validation
paths:
  /api/campaigns/{campaignId}/send:
    post:
      tags:
        - Campaigns
      summary: Send a webhook campaign message to one recipient
      description: >-
        Requires a campaign-scoped key. Charges the campaign wallet per call;
        failed sends are refunded.
      operationId: sendCampaignMessage
      parameters:
        - name: campaignId
          in: path
          required: true
          description: >-
            The campaign's ID. The campaign page's Contract tab shows the full
            URL for your campaign.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CampaignRequest'
      responses:
        '200':
          description: Message sent and charged
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    const: sent
                    description: >-
                      Always sent on a successful call. The message was accepted
                      and the wallet was charged.
                  quoted_usd:
                    type: number
                    description: >-
                      What this send cost in US dollars, charged to the campaign
                      wallet.
                  balance:
                    type: number
                    description: >-
                      The campaign wallet balance in US dollars after the
                      charge.
                  tars_request_id:
                    type: string
                    description: >-
                      The tars_… request ID. Quote it when contacting support
                      about this send.
                  external_message_id:
                    type: string
                    description: The provider's message ID, when the provider returned one.
                required:
                  - status
                  - quoted_usd
                  - tars_request_id
        '400':
          description: campaign_mode_invalid — not a webhook campaign
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '401':
          description: unauthorized or invalid_key
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '402':
          description: insufficient_balance
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '403':
          description: consent_revoked, us_marketing_blocked, or channel_test_mode
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '404':
          description: not_found — unknown route or campaign
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: >-
            campaign_paused or channel_unavailable (retryable), or
            template_version_drift (not retryable). Retry-After is sent only
            when the provider reported a wait.
          headers:
            Retry-After:
              description: Seconds to wait before retrying
              schema:
                type: string
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '410':
          description: >-
            campaign_gone — campaign cancelled or failed; terminal, stop
            retrying
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          description: >-
            invalid_params (see errors array), template_not_approved,
            channel_misconfigured, channel_reauth_required, or meta_rejected
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '429':
          description: rate_limited or meta_rate_limited (retryable)
          headers:
            Retry-After:
              description: Seconds to wait before retrying
              schema:
                type: string
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '500':
          description: internal_error
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '503':
          description: meta_unavailable (retryable)
          headers:
            Retry-After:
              description: Seconds to wait before retrying
              schema:
                type: string
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
components:
  schemas:
    CampaignRequest:
      type: object
      description: One webhook campaign send.
      properties:
        to:
          type: string
          description: Recipient phone number in international format
        params:
          type: object
          additionalProperties:
            type: string
          description: Template parameters keyed by slot name
        media:
          type: object
          description: >-
            Media for the template's media slot. Sending it to a template with
            no media slot returns unknown_param on media.url.
          properties:
            url:
              type: string
              description: Publicly reachable URL of the media file.
          required:
            - url
      required:
        - to
    Problem:
      type: object
      description: RFC 9457 problem details, returned by the webhook campaign endpoints.
      properties:
        title:
          type: string
          description: A short summary of the problem type.
        status:
          type: integer
          description: The HTTP status code, repeated in the body.
        detail:
          type: string
          description: A sentence describing this specific failure.
        instance:
          type: string
          description: The tars_… request ID for log correlation
        code:
          type: string
          description: A stable machine-readable code from the campaign error catalog.
          enum:
            - not_found
            - unauthorized
            - invalid_key
            - organization_inactive
            - billing_locked
            - rate_limited
            - invalid_params
            - campaign_mode_invalid
            - campaign_paused
            - campaign_gone
            - template_version_drift
            - template_not_approved
            - consent_revoked
            - us_marketing_blocked
            - channel_misconfigured
            - channel_unavailable
            - channel_reauth_required
            - channel_test_mode
            - insufficient_balance
            - meta_rejected
            - meta_rate_limited
            - meta_unavailable
            - internal_error
        retryable:
          type: boolean
          description: true when retrying the same call can succeed
        errors:
          type: array
          description: Per-field problems, when the call failed validation.
          items:
            $ref: '#/components/schemas/CampaignFieldError'
      required:
        - title
        - status
        - detail
        - instance
        - code
        - retryable
    CampaignFieldError:
      type: object
      description: One field that failed campaign validation.
      properties:
        field:
          type: string
          description: >-
            The request field that failed, for example params.order_id or
            media.url.
        code:
          type: string
          description: Why the field failed.
          enum:
            - template_param_missing
            - template_param_invalid
            - unsupported_template
            - unknown_param
            - invalid_e164
            - invalid_media
      required:
        - field
        - code
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        API key sent as Authorization: Bearer YOUR_API_KEY. Campaign endpoints
        use campaign-scoped keys; the rest use organization or agent keys with
        the matching permission.

````