Send Message

POST

api.ziett.co/c/v1 /messages

Submit a request to send a single transactional message to a specific recipient. Designed for high-reliability, low-latency programmatic notifications — OTPs, delivery alerts, account events, and other time-sensitive communications that must reach one user immediately.

One endpoint, multiple channels. You choose the channel with channel_type; the rest of the request shape stays the same.

For sending messages to large audiences, see Send Batch Campaign.

Choose your channel

SMSWhatsApp
channel_typeSMSWHATSAPP
Message sourcecontent (free text) or template_idtemplate_id (a template approved by Meta)
VariablesOptional, when using a templateRequired whenever the template declares variables
DeliveryCarrier networkMeta Cloud API

Jump to: SMS · WhatsApp


How It Works

  1. Validation — The API authenticates your key, verifies the messages:send scope, and validates all fields.
  2. Template resolution — If a template_id is provided, the template is loaded and its variables are resolved (see Template Variables).
  3. Routing — The recipient's number is analysed to identify the target carrier network and country.
  4. Pricing — The cost for the specific route and channel is calculated against your plan.
  5. Funds Check — The system verifies your balance is sufficient. If not, the request is rejected with 402.
  6. Queuing — The message is placed in a high-priority delivery queue and dispatched to the carrier or Meta's Cloud API.
  7. Response — 202 Accepted is returned immediately with a message_id to track the lifecycle.

202 Accepted — not 200 OK — is intentional. It means your message has been accepted and queued, not yet delivered. Track final status via webhook or Retrieve Message.


Request

Required scope: apikey:messages:send

Body

Content-Type: application/json

FieldTypeRequiredDescription
remitter_iduuid✅UUID of the Sender ID configured in your organization. Determines the sender name or number displayed to the recipient.
channel_typeenum✅Delivery channel. Accepted values: SMS, WHATSAPP.
target_e164string✅Recipient phone number in E.164 format (e.g., +244990090990). Must include the country code.
contentstring⚠️Message body for free-text SMS. Max 1600 characters — multi-part messages are concatenated automatically. Not used for WhatsApp.
template_iduuid⚠️UUID of a template to send. Required for WhatsApp. For SMS, use it instead of content.
variablesobject❌Values for the template's variables. See Template Variables.
save_contactobject❌If provided, upserts the recipient into your contact list. See Save Contact Object.

⚠️ Provide either content or template_id, depending on the channel — see the channel sections below.

Save Contact Object

When included, the recipient is created or updated in your contacts list at send time — useful for building your audience organically from transactional traffic. Works the same way on every channel.

FieldTypeRequiredDescription
namestring✅Full name of the contact.
emailstring❌Email address. Must be a valid format.
tagsarray[string]❌Up to 10 tags to assign (e.g., ["Premium", "Angola"]). Created automatically if they don't exist.

Template Variables

When you send from a template, the variables object fills in its placeholders. Values can be sent literally in values, read from the recipient's contact with mapping, or matched automatically by name. If a value can't be resolved and strict is true (the default), the request fails with a 422.

The full rules (resolution order, contact lookup, key format per channel) are in Templates & Variables.


Sending SMS

Set channel_type to SMS and provide the text with either:

  • content — free text, sent as-is; or
  • template_id — a saved template, optionally with variables.

Examples

import httpx

response = httpx.post(
    "https://api.ziett.co/c/v1/messages",
    headers={
        "X-API-KEY": "zk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "Content-Type": "application/json",
    },
    json={
        "remitter_id": "019b4b56-e704-7c30-83a0-527df63c3e00",
        "channel_type": "SMS",
        "target_e164": "+244990090990",
        "content": "Your verification code is 482910. It expires in 5 minutes.",
    },
)

if response.status_code == 202:
    message_id = response.json()["message_id"]
    print(f"Message accepted. Tracking ID: {message_id}")
else:
    print(f"Error: {response.json()}")

Sending WhatsApp

Set channel_type to WHATSAPP and provide a template_id. WhatsApp messages are sent through Meta's Cloud API, so the content comes from a template approved by Meta; your request only supplies the values for its variables.

remitter_id for WhatsApp. Use the Ziett ID of the WhatsApp number linked to your organization — the internal Ziett identifier, not the phone number ID from Meta.

Variable keys

Keys in values and mapping are written as <scope>:<name>, one scope per part of the template:

  • header:1 or header:<name> — header placeholders.
  • body:1, body:2, … or body:<name> — body placeholders.
  • button:0, button:1, … — button parameters.

Buttons start at 0. Header and body placeholders are numbered from 1, but the first button is button:0.

Use numbers for positional templates ({{1}}) and names for named templates ({{customer_name}}). See Templates & Variables for the full table.

Examples

Named template. Body declares {{customer_name}} and {{order_id}}; the customer's name is read from the contact, the order ID is sent literally.

curl -X POST https://api.ziett.co/c/v1/messages \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: zk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -d '{
    "remitter_id": "019b4b56-e704-7c30-83a0-527df63c3e00",
    "template_id": "019c2f7d-8a31-7b02-b6c5-1e94d0a37f58",
    "channel_type": "WHATSAPP",
    "target_e164": "+244990090990",
    "variables": {
      "values": { "body:order_id": "A-10293" },
      "mapping": {
        "body:customer_name": { "attribute": "first_name", "default": "customer" }
      }
    }
  }'

Positional template with header and button. Header has {{1}}, body has {{1}} and {{2}}, and the first button is a dynamic URL.

curl -X POST https://api.ziett.co/c/v1/messages \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: zk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -d '{
    "remitter_id": "019b4b56-e704-7c30-83a0-527df63c3e00",
    "template_id": "019c3a90-2d44-7e19-a8f3-5b7c60d1e2a6",
    "channel_type": "WHATSAPP",
    "target_e164": "+244990090990",
    "variables": {
      "values": {
        "header:1": "Order A-10293",
        "body:1": "Carlos",
        "body:2": "Friday, 18 July",
        "button:0": "track/A-10293"
      }
    }
  }'

Lenient mode. With strict set to false, variables that can't be resolved are sent as empty strings instead of rejecting the request.

{
  "variables": {
    "values": { "body:1": "Carlos" },
    "strict": false
  }
}

Prefer the default strict: true for transactional traffic such as OTPs and payment alerts, where a message with a missing value is worse than a rejected request.


Responses

202 Accepted — Message Queued

The message has been validated, your account debited, and the message placed in the delivery queue. The response is the same for every channel.

{
  "message_id": "019b4b56-e704-7c30-83a0-527df63c3e00"
}
FieldTypeDescription
message_iduuidUnique identifier for this message. Store it to retrieve delivery status or correlate with webhook events.

Errors

All errors share the same body shape: code, message, status, trace_id, timestamp and service. Include the trace_id when contacting support.

401 Unauthorized — Invalid API Key

The X-API-KEY header is missing, malformed, or the key has been revoked.

{
  "code": "AUTH_INVALID_API_KEY",
  "message": "The provided API key is invalid or has been revoked.",
  "status": 401,
  "trace_id": "a1b2c3d4e5f644358a9e7011c123c831",
  "timestamp": "2025-07-14T10:30:00.000000+00:00",
  "service": "core"
}

402 Payment Required — Insufficient Funds

Your organization's credit balance is below the cost of this message route.

{
  "code": "BILLING_INSUFFICIENT_FUNDS",
  "message": "Your account balance is too low to process this request. Please top up your credits.",
  "status": 402,
  "trace_id": "c109f78ae57744358a9e7011c123c831",
  "timestamp": "2025-07-14T10:30:00.000000+00:00",
  "service": "billing"
}

422 Unprocessable Entity — Validation Error

One or more fields are missing or contain invalid values. The fields object maps each failing field to a specific message.

{
  "code": "VALIDATION_ERROR",
  "message": "One or more fields failed validation.",
  "status": 422,
  "trace_id": "f812a3bc91e044358a9e7011c456d901",
  "timestamp": "2025-07-14T10:30:00.000000+00:00",
  "service": "core",
  "fields": {
    "target_e164": "Must be a valid E.164 formatted phone number (e.g., +244990090990).",
    "channel_type": "Accepted values are: SMS, WHATSAPP."
  }
}

Unresolved template variables. With strict: true (the default), a variable that no strategy could resolve is reported in fields, keyed by the variable key:

{
  "code": "VALIDATION_ERROR",
  "message": "One or more fields failed validation.",
  "status": 422,
  "trace_id": "9be41d07a3c244358a9e7011c8f2a105",
  "timestamp": "2025-07-14T10:30:00.000000+00:00",
  "service": "core",
  "fields": {
    "variables": "Could not resolve a value for: body:customer_name."
  }
}

To fix it, send the value in variables.values, add a mapping with a default, make sure the contact has the attribute, or set strict to false.


Rate Limits

This endpoint is limited to 120 requests per minute per API Key, shared across channels. See Rate Limits & Reliability for backoff strategies and retry guidance.