Skip to content

Send a message ​

POST/v1/messages

Sends a WhatsApp or SMS message with a template of the account to one number. An SMS may carry an own text instead, when the account allows it.

202 Accepted means the message passed every check, its price is fixed and reserved, and it is queued (status is queued). Its statuses follow as webhooks and in GET /messages/{id}. A refused request creates no message and costs nothing.

Send an Idempotency-Key header to retry safely: a repeat with the same key and body within 24 hours answers the first message with the header Idempotent-Replayed: true. A test key sends through the sandbox: free, price null, test true, at most 1000 messages a day.

Guides: https://docs.ace-send.com/guide/sending, https://docs.ace-send.com/guide/sms.

API key permission: send · Limit: 50 requests a second per key

Headers ​

FieldTypeRequiredDescription
Idempotency-Keystring1–64 printable ASCII characters. A repeat with the same key and body within 24 hours answers the same message; another body — 409 idempotency_conflict. Pattern: ^[!-~]{1,64}$.

Request body ​

A message with a template (template, language, variables) or, for an SMS, with an own text — exactly one of the two: both answer text prohibited, neither answers template required.

FieldTypeRequiredDescription
tostringyesThe number in E.164, for example +5521998120937. Up to 32 characters.
templatestringThe name of an approved template of the channel (GET /templates); or text, never both. Up to 64 characters.
languagestringA language of the template; not with text. Up to 16 characters.
variablesobjectValues of the variables by key; a missing one takes the fallback of the template in the language. Not with text. Keys map to string values.
referencestring, nullableYour own id of the message (up to 64 characters), returned with it and searchable. Up to 64 characters.
channelstringOne of: whatsapp, sms.
textstring, nullableAn own SMS text instead of a template (channel sms) when the account may send one (otherwise 403 free_text_not_allowed); split into segments as any SMS. Not with template. Up to 10000 characters.

Request ​

WhatsApp template

bash
curl -X POST "https://api.ace-send.com/v1/messages" \
  -H "Authorization: Bearer ak_live_YOUR_API_KEY" \
  -H "Idempotency-Key: crm-42-welcome" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+5521998120937",
    "template": "welcome_bonus",
    "language": "pt",
    "variables": {
      "name": "Ana",
      "promo_code": "BONUS10"
    },
    "reference": "crm-42"
  }'
js
const response = await fetch('https://api.ace-send.com/v1/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ACE_SEND_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'crm-42-welcome',
  },
  body: JSON.stringify({
    "to": "+5521998120937",
    "template": "welcome_bonus",
    "language": "pt",
    "variables": {
      "name": "Ana",
      "promo_code": "BONUS10"
    },
    "reference": "crm-42"
  }),
})
const result = await response.json()

if (!response.ok) {
  // application/problem+json: result.code is stable, result.title is for people
  throw new Error(`${result.code}: ${result.title}`)
}
console.log(result)
python
import os

import requests

headers = {
    "Authorization": f"Bearer {os.environ['ACE_SEND_API_KEY']}",
    "Idempotency-Key": "crm-42-welcome",
}
response = requests.post(
    "https://api.ace-send.com/v1/messages",
    headers=headers,
    json={
        "to": "+5521998120937",
        "template": "welcome_bonus",
        "language": "pt",
        "variables": {
            "name": "Ana",
            "promo_code": "BONUS10",
        },
        "reference": "crm-42",
    },
    timeout=10,
)
result = response.json()

if not response.ok:
    # application/problem+json: result["code"] is stable, result["title"] is for people
    raise RuntimeError(f"{result['code']}: {result['title']}")
print(result)
php
<?php

// composer require guzzlehttp/guzzle
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.ace-send.com/v1/']);

$response = $client->request('POST', 'messages', [
    'headers' => [
        'Authorization' => 'Bearer '.getenv('ACE_SEND_API_KEY'),
        'Idempotency-Key' => 'crm-42-welcome',
    ],
    'json' => [
        'to' => '+5521998120937',
        'template' => 'welcome_bonus',
        'language' => 'pt',
        'variables' => [
            'name' => 'Ana',
            'promo_code' => 'BONUS10',
        ],
        'reference' => 'crm-42',
    ],
    'http_errors' => false,
]);
$result = json_decode((string) $response->getBody(), true);

if ($response->getStatusCode() >= 400) {
    // application/problem+json: $result['code'] is stable, $result['title'] is for people
    throw new RuntimeException($result['code'].': '.$result['title']);
}
print_r($result);

SMS template

bash
curl -X POST "https://api.ace-send.com/v1/messages" \
  -H "Authorization: Bearer ak_live_YOUR_API_KEY" \
  -H "Idempotency-Key: crm-42-welcome" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "to": "+5511912345678",
    "template": "deposit_reminder",
    "language": "en",
    "variables": {
      "name": "Ann",
      "amount": "$50.00"
    }
  }'
js
const response = await fetch('https://api.ace-send.com/v1/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ACE_SEND_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'crm-42-welcome',
  },
  body: JSON.stringify({
    "channel": "sms",
    "to": "+5511912345678",
    "template": "deposit_reminder",
    "language": "en",
    "variables": {
      "name": "Ann",
      "amount": "$50.00"
    }
  }),
})
const result = await response.json()

if (!response.ok) {
  // application/problem+json: result.code is stable, result.title is for people
  throw new Error(`${result.code}: ${result.title}`)
}
console.log(result)
python
import os

import requests

headers = {
    "Authorization": f"Bearer {os.environ['ACE_SEND_API_KEY']}",
    "Idempotency-Key": "crm-42-welcome",
}
response = requests.post(
    "https://api.ace-send.com/v1/messages",
    headers=headers,
    json={
        "channel": "sms",
        "to": "+5511912345678",
        "template": "deposit_reminder",
        "language": "en",
        "variables": {
            "name": "Ann",
            "amount": "$50.00",
        },
    },
    timeout=10,
)
result = response.json()

if not response.ok:
    # application/problem+json: result["code"] is stable, result["title"] is for people
    raise RuntimeError(f"{result['code']}: {result['title']}")
print(result)
php
<?php

// composer require guzzlehttp/guzzle
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.ace-send.com/v1/']);

$response = $client->request('POST', 'messages', [
    'headers' => [
        'Authorization' => 'Bearer '.getenv('ACE_SEND_API_KEY'),
        'Idempotency-Key' => 'crm-42-welcome',
    ],
    'json' => [
        'channel' => 'sms',
        'to' => '+5511912345678',
        'template' => 'deposit_reminder',
        'language' => 'en',
        'variables' => [
            'name' => 'Ann',
            'amount' => '$50.00',
        ],
    ],
    'http_errors' => false,
]);
$result = json_decode((string) $response->getBody(), true);

if ($response->getStatusCode() >= 400) {
    // application/problem+json: $result['code'] is stable, $result['title'] is for people
    throw new RuntimeException($result['code'].': '.$result['title']);
}
print_r($result);

SMS with an own text

bash
curl -X POST "https://api.ace-send.com/v1/messages" \
  -H "Authorization: Bearer ak_live_YOUR_API_KEY" \
  -H "Idempotency-Key: crm-42-welcome" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "to": "+5511912345678",
    "text": "Your ACE SEND code is 482913. It expires in 10 minutes.",
    "reference": "otp-7781"
  }'
js
const response = await fetch('https://api.ace-send.com/v1/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ACE_SEND_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'crm-42-welcome',
  },
  body: JSON.stringify({
    "channel": "sms",
    "to": "+5511912345678",
    "text": "Your ACE SEND code is 482913. It expires in 10 minutes.",
    "reference": "otp-7781"
  }),
})
const result = await response.json()

if (!response.ok) {
  // application/problem+json: result.code is stable, result.title is for people
  throw new Error(`${result.code}: ${result.title}`)
}
console.log(result)
python
import os

import requests

headers = {
    "Authorization": f"Bearer {os.environ['ACE_SEND_API_KEY']}",
    "Idempotency-Key": "crm-42-welcome",
}
response = requests.post(
    "https://api.ace-send.com/v1/messages",
    headers=headers,
    json={
        "channel": "sms",
        "to": "+5511912345678",
        "text": "Your ACE SEND code is 482913. It expires in 10 minutes.",
        "reference": "otp-7781",
    },
    timeout=10,
)
result = response.json()

if not response.ok:
    # application/problem+json: result["code"] is stable, result["title"] is for people
    raise RuntimeError(f"{result['code']}: {result['title']}")
print(result)
php
<?php

// composer require guzzlehttp/guzzle
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.ace-send.com/v1/']);

$response = $client->request('POST', 'messages', [
    'headers' => [
        'Authorization' => 'Bearer '.getenv('ACE_SEND_API_KEY'),
        'Idempotency-Key' => 'crm-42-welcome',
    ],
    'json' => [
        'channel' => 'sms',
        'to' => '+5511912345678',
        'text' => 'Your ACE SEND code is 482913. It expires in 10 minutes.',
        'reference' => 'otp-7781',
    ],
    'http_errors' => false,
]);
$result = json_decode((string) $response->getBody(), true);

if ($response->getStatusCode() >= 400) {
    // application/problem+json: $result['code'] is stable, $result['title'] is for people
    throw new RuntimeException($result['code'].': '.$result['title']);
}
print_r($result);

Response ​

202 Accepted ​

FieldTypeDescription
dataMessage
json
{
  "data": {
    "id": "msg_CgdWeHQN4sPaZnXfh7kP5",
    "status": "queued",
    "to": "+5521998120937",
    "from": null,
    "country": "BR",
    "channel": "whatsapp",
    "template": {
      "name": "welcome_bonus",
      "version": 3
    },
    "language": "pt",
    "reference": "crm-42",
    "text": "Olá Ana! Seu bônus BONUS10 já está ativo.",
    "text_masked": false,
    "segments": 1,
    "encoding": null,
    "price": {
      "amount": "0.062500",
      "currency": "USD"
    },
    "error": null,
    "test": false,
    "campaign": null,
    "created_at": "2026-11-02T12:00:00+00:00",
    "updated_at": "2026-11-02T12:00:00+00:00",
    "events": null
  }
}
json
{
  "data": {
    "id": "msg_CgdWeHQNnp4VN7KkfNqju",
    "status": "queued",
    "to": "+5511912345678",
    "from": "ACESEND",
    "country": "BR",
    "channel": "sms",
    "template": null,
    "language": null,
    "reference": "otp-7781",
    "text": "Your ACE SEND code is 482913. It expires in 10 minutes.",
    "text_masked": false,
    "segments": 1,
    "encoding": "gsm7",
    "price": {
      "amount": "0.018000",
      "currency": "USD"
    },
    "error": null,
    "test": false,
    "campaign": null,
    "created_at": "2026-11-02T12:00:00+00:00",
    "updated_at": "2026-11-02T12:00:00+00:00",
    "events": null
  }
}

Objects ​

Message object ​

FieldTypeDescription
idstringThe message, msg_….
statusstringOne of: accepted, queued, submitted, sent, delivered, read, rejected, failed, expired, cancelled.
tostringThe number in E.164.
fromstring, nullableThe sender name of an SMS; null for WhatsApp (its number is not shown) and for a test SMS.
countrystringISO 3166-1 alpha-2 of the number.
channelstringOne of: whatsapp, sms.
templateobject, nullableThe template and the number of the version the message was sent with; null for an own SMS text.
template.namestring
template.versioninteger
languagestring, nullableThe language of the template; null for an own SMS text.
referencestring, nullableYour own id of the message, as you sent it.
textstring, nullableThe text as sent; null once it is past its retention period (90 days, text_masked).
text_maskedbooleanWhether text was erased after the retention period (90 days).
segmentsintegerThe parts the SMS is sent and priced in (its price is the price of a segment times them); 1 for WhatsApp.
encodingstring, nullableThe encoding of the SMS: gsm7 (160 characters, 153 a segment of a longer text) or ucs2 (70, 67); null for WhatsApp. One of: gsm7, ucs2.
priceobject, nullableThe price fixed at acceptance, in the currency of the account; for an SMS, the price of a segment times segments. Null for a test message.
price.amountstring
price.currencystring
errorobject, nullableWhy the message did not reach the recipient: a stable code and a text in the language of Accept-Language. Null while it is on its way or when delivered.
error.codestring
error.textstring
testbooleanSent with a test key: through the sandbox, free.
campaignobject, nullableThe console campaign that sent the message; null for a message of the API.
campaign.idstring
campaign.namestring
created_atstringWhen the message was accepted.
updated_atstringWhen the message last changed.
eventsobject[], nullableThe history of statuses, oldest first. Only in GET /messages/{id}; null elsewhere.
events[].statusstring
events[].atstring
events[].errorobject, nullable
events[].error.codestring
events[].error.textstring

Errors ​

Errors are application/problem+json with a stable code (all codes). Besides the errors of every request (an invalid key, a missing permission, too many requests), this endpoint answers:

StatusCodeMeaning
422validation_failedSome fields are wrong: errors lists each field with its code and a message.
422invalid_numberThe number is not a valid phone number. Send it in E.164: +, the country code and the number, without spaces or dashes.
404template_not_foundThe account has no approved template with this name in the channel.
409template_unavailableWhatsApp only: the template version is approved in ACE SEND but not (or no longer) approved by WhatsApp for sending, for example paused or disabled. Check the template in the console.
403free_text_not_allowedAn SMS with an own text, but the account may only send templates. Ask your manager to enable it.
422message_too_longThe SMS needs more segments than allowed; segments and max_segments say by how much.
422recipient_stoppedThe number is on one of the stop lists of the account (opted out or added by your team).
422destination_unavailableThe channel does not deliver to this country for the account: no route, no price, or (SMS) no sender name for the country. Ask your manager.
409insufficient_fundsThe available balance does not cover the price; shortage is the missing amount. Top up and retry.
429daily_limit_reachedThe daily message limit of the account (or of test keys: 1000 a day) is reached. It resets at midnight in the time zone of the account; retry_after and the Retry-After header say when. Ask your manager to raise the limit.
409idempotency_conflictThe Idempotency-Key was used within 24 hours for another body, or by a key of the other mode (test or live).
409idempotency_in_progressThe first request with this Idempotency-Key is still running. Retry in a moment.
json
{
  "type": "about:blank",
  "status": 422,
  "code": "validation_failed",
  "title": "Check the form fields",
  "errors": [
    {
      "field": "variables.promo_code",
      "code": "required",
      "message": "Fill in this field"
    }
  ]
}

ACE SEND API v1