> This page is for Omni Channel.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.hams.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.hams.ai/_mcp/server.

# Send a message

POST https://omni-channel.hams.ai/api/v1/accounts/{account_id}/developer/messages
Content-Type: application/json

Sends an outgoing message to a conversation, a contact, or a phone number. The API creates the contact and the conversation when they do not exist. Before it accepts plain `content`, the API checks the service window of the conversation. A `201` means that the API saved the message and put it in the delivery queue. It does not mean that the channel delivered it.


Reference: https://docs.hams.ai/omni-channel/reference/messaging/send-message

## Authentication

- `api_access_token` header (required) — An API key that an administrator creates in **Settings** > **Developer**. The personal access token of a user does not work. A key reaches every inbox, conversation, and contact in the account that created it, and nothing in other accounts.

## Request

### Path parameters

- `account_id` (integer, required) — The id of your account. It is the number after `/accounts/` in the dashboard URL.

### Body (application/json)

This endpoint expects a SendMessageRequest.

- `conversation_id` (integer, optional) — The conversation number in the dashboard URL (`display_id`). The conversation gives the inbox, so `inbox_id` is not necessary.
- `contact_id` (integer, optional) — The id of a contact. The API uses the most recent conversation of the contact on `inbox_id` with a status other than `resolved`, or it opens a new conversation. The contact must have an address that the channel can reach. If it has none, the API returns `contact_not_messageable`.
- `phone_number` (string, optional) — A phone number in E.164 format, for example `+971500000000`. The API creates the contact if it does not exist. Only WhatsApp, Twilio, and SMS inboxes accept a phone number.
- `inbox_id` (integer, optional) — The inbox that sends the message. Necessary if the request has no `conversation_id`.
- `content` (string, optional) — The text of the message. Optional when `template_params` is present on a WhatsApp inbox: the API then makes the text from the body of the approved template. A Twilio inbox always needs `content`.
- `template_params` (TemplateParams, optional) — Sends an approved WhatsApp template instead of free text. A template is necessary to start a conversation on WhatsApp, or to send after the 24-hour service window closes. When this field is present, the API does not check the service window on any channel. Send it only on an inbox that has approved templates.
- `contact` (SendMessageRequestContact, optional) — The fields of a new contact. The API reads them only with `phone_number`, and only when no contact has that number.

## Response

### 201

The API saved the message and put it in the delivery queue.

- `id` (integer, optional) — The id of the message.
- `conversation_id` (integer, optional) — The conversation number in the dashboard URL (`display_id`).
- `contact_id` (integer, optional) — The id of the contact.
- `inbox_id` (integer, optional) — The id of the inbox.
- `status` (enum, optional) — The delivery status at the time of the response. A new message is `sent`. The dashboard shows the later status.
  - Allowed values: `sent`, `delivered`, `read`, `failed`
- `content` (string, optional) — The text of the message.
- `created_at` (integer, optional) — The time of the message, as a Unix timestamp in seconds.

## Errors

### 401 Unauthorized Error

The token does not exist, is not an API key, or belongs to another account. The token can also be the token of a user who is not a member of the account, or the account can be suspended. The body has only the `error` field.

- `error` (string, required) — `Invalid Access Token`, `Key is not authorized to access this account`, `Access to this endpoint is not authorized for this token`, `You are not authorized to access this account`, or `Account is suspended`.

### 404 Not Found Error

The conversation, contact, inbox, or phone number is not in this account. `error` is `not_found`.

- `error` (string, required) — A stable code for a program, for example `outside_messaging_window`. Branch on this field.
- `message` (string, required) — A message for a person. It can change at any time.

### 422 Unprocessable Entity Error

The API understood the request but cannot do it. `error` is `identifier_required`, `inbox_id_required`, `content_required`, `invalid_phone_number`, `unsupported_channel`, `contact_not_messageable`, `outside_messaging_window`, or `contact_conflict`. A message that is not valid, for example a send with `template_params` to the Phone inbox, returns a `ValidationError` body instead. A send with plain `content` to the Phone inbox returns `outside_messaging_window`.

- `SendMessageRequestUnprocessableEntityError`
  - Error: Error
  - ValidationError: ValidationError

## Types

### TemplateParams

Sends an approved WhatsApp template instead of free text. A template is necessary to start a conversation on WhatsApp, or to send after the 24-hour service window closes. When this field is present, the API does not check the service window on any channel. Send it only on an inbox that has approved templates.

- `name` (string, required) — The name of the approved template.
- `language` (string, required) — The language code of the template, for example `en` or `en_US`. Send the code exactly as the template list in the dashboard shows it.
- `category` (string, optional) — The category of the template, for example `UTILITY` or `MARKETING`.
- `processed_params` (map from string to any, optional) — The values for the variables of the template. A WhatsApp Cloud positional template takes `{"body": {"1": "121212"}}`. A named template takes `{"body": {"order_id": "121212"}}`. `header`, `footer`, and `buttons` are also possible. A Twilio template takes a flat map, for example `{"1": "121212"}`.

### SendMessageRequestContact

The fields of a new contact. The API reads them only with `phone_number`, and only when no contact has that number.

- `name` (string, optional)
- `email` (string, optional)
- `custom_attributes` (map from string to any, optional)

### Error

The error body of the send endpoint, the pre-call context endpoint, and the phone number contact endpoints.

- `error` (string, required) — A stable code for a program, for example `outside_messaging_window`. Branch on this field.
- `message` (string, required) — A message for a person. It can change at any time.

### ValidationError

A field that is not valid on a conversation or a contact.

- `message` (string, required) — The problems, with a comma between them. For example `Email has already been taken`.
- `attributes` (list of string, required) — The names of the fields that are not valid.

## Examples

### Template to a new phone number

**Request**

```json
{
  "phone_number": "+971500000000",
  "inbox_id": 5,
  "template_params": {
    "name": "order_confirmation",
    "language": "en",
    "category": "UTILITY",
    "processed_params": {
      "body": {
        "1": "121212"
      }
    }
  }
}
```

**Response**

```json
{
  "id": 1,
  "conversation_id": 1,
  "contact_id": 1,
  "inbox_id": 1,
  "status": "sent",
  "content": "string",
  "created_at": 1
}
```

**SDK Code**

```python Template to a new phone number
import requests

url = "https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages"

payload = {
    "phone_number": "+971500000000",
    "inbox_id": 5,
    "template_params": {
        "name": "order_confirmation",
        "language": "en",
        "category": "UTILITY",
        "processed_params": { "body": { "1": "121212" } }
    }
}
headers = {
    "api_access_token": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript Template to a new phone number
const url = 'https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages';
const options = {
  method: 'POST',
  headers: {api_access_token: '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"phone_number":"+971500000000","inbox_id":5,"template_params":{"name":"order_confirmation","language":"en","category":"UTILITY","processed_params":{"body":{"1":"121212"}}}}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Template to a new phone number
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages"

	payload := strings.NewReader("{\n  \"phone_number\": \"+971500000000\",\n  \"inbox_id\": 5,\n  \"template_params\": {\n    \"name\": \"order_confirmation\",\n    \"language\": \"en\",\n    \"category\": \"UTILITY\",\n    \"processed_params\": {\n      \"body\": {\n        \"1\": \"121212\"\n      }\n    }\n  }\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("api_access_token", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Template to a new phone number
require 'uri'
require 'net/http'

url = URI("https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["api_access_token"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"phone_number\": \"+971500000000\",\n  \"inbox_id\": 5,\n  \"template_params\": {\n    \"name\": \"order_confirmation\",\n    \"language\": \"en\",\n    \"category\": \"UTILITY\",\n    \"processed_params\": {\n      \"body\": {\n        \"1\": \"121212\"\n      }\n    }\n  }\n}"

response = http.request(request)
puts response.read_body
```

```java Template to a new phone number
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages")
  .header("api_access_token", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"phone_number\": \"+971500000000\",\n  \"inbox_id\": 5,\n  \"template_params\": {\n    \"name\": \"order_confirmation\",\n    \"language\": \"en\",\n    \"category\": \"UTILITY\",\n    \"processed_params\": {\n      \"body\": {\n        \"1\": \"121212\"\n      }\n    }\n  }\n}")
  .asString();
```

```php Template to a new phone number
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages', [
  'body' => '{
  "phone_number": "+971500000000",
  "inbox_id": 5,
  "template_params": {
    "name": "order_confirmation",
    "language": "en",
    "category": "UTILITY",
    "processed_params": {
      "body": {
        "1": "121212"
      }
    }
  }
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'api_access_token' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp Template to a new phone number
using RestSharp;

var client = new RestClient("https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages");
var request = new RestRequest(Method.POST);
request.AddHeader("api_access_token", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"phone_number\": \"+971500000000\",\n  \"inbox_id\": 5,\n  \"template_params\": {\n    \"name\": \"order_confirmation\",\n    \"language\": \"en\",\n    \"category\": \"UTILITY\",\n    \"processed_params\": {\n      \"body\": {\n        \"1\": \"121212\"\n      }\n    }\n  }\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift Template to a new phone number
import Foundation

let headers = [
  "api_access_token": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = [
  "phone_number": "+971500000000",
  "inbox_id": 5,
  "template_params": [
    "name": "order_confirmation",
    "language": "en",
    "category": "UTILITY",
    "processed_params": ["body": ["1": "121212"]]
  ]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Reply in an open conversation

**Request**

```json
{
  "conversation_id": 1234,
  "content": "It is on its way."
}
```

**Response**

```json
{
  "id": 1,
  "conversation_id": 1,
  "contact_id": 1,
  "inbox_id": 1,
  "status": "sent",
  "content": "string",
  "created_at": 1
}
```

**SDK Code**

```python Reply in an open conversation
import requests

url = "https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages"

payload = {
    "conversation_id": 1234,
    "content": "It is on its way."
}
headers = {
    "api_access_token": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript Reply in an open conversation
const url = 'https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages';
const options = {
  method: 'POST',
  headers: {api_access_token: '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"conversation_id":1234,"content":"It is on its way."}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Reply in an open conversation
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages"

	payload := strings.NewReader("{\n  \"conversation_id\": 1234,\n  \"content\": \"It is on its way.\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("api_access_token", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Reply in an open conversation
require 'uri'
require 'net/http'

url = URI("https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["api_access_token"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"conversation_id\": 1234,\n  \"content\": \"It is on its way.\"\n}"

response = http.request(request)
puts response.read_body
```

```java Reply in an open conversation
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages")
  .header("api_access_token", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"conversation_id\": 1234,\n  \"content\": \"It is on its way.\"\n}")
  .asString();
```

```php Reply in an open conversation
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages', [
  'body' => '{
  "conversation_id": 1234,
  "content": "It is on its way."
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'api_access_token' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp Reply in an open conversation
using RestSharp;

var client = new RestClient("https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages");
var request = new RestRequest(Method.POST);
request.AddHeader("api_access_token", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"conversation_id\": 1234,\n  \"content\": \"It is on its way.\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift Reply in an open conversation
import Foundation

let headers = [
  "api_access_token": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = [
  "conversation_id": 1234,
  "content": "It is on its way."
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://omni-channel.hams.ai/api/v1/accounts/1/developer/messages")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```