Send Message
curl --request POST \
--url https://app.puffle.ai/api/threads/{id}/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"body_text": "<string>",
"body_html": "<string>",
"subject": "<string>",
"account_id": "<string>",
"cc": "<string>",
"bcc": "<string>",
"attachments": [
{
"name": "<string>",
"bucket": "<string>",
"path": "<string>",
"url": "<string>",
"mime_type": "<string>",
"size": 1,
"content_id": "<string>"
}
]
}
'import requests
url = "https://app.puffle.ai/api/threads/{id}/messages"
payload = {
"body_text": "<string>",
"body_html": "<string>",
"subject": "<string>",
"account_id": "<string>",
"cc": "<string>",
"bcc": "<string>",
"attachments": [
{
"name": "<string>",
"bucket": "<string>",
"path": "<string>",
"url": "<string>",
"mime_type": "<string>",
"size": 1,
"content_id": "<string>"
}
]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
body_text: '<string>',
body_html: '<string>',
subject: '<string>',
account_id: '<string>',
cc: '<string>',
bcc: '<string>',
attachments: [
{
name: '<string>',
bucket: '<string>',
path: '<string>',
url: '<string>',
mime_type: '<string>',
size: 1,
content_id: '<string>'
}
]
})
};
fetch('https://app.puffle.ai/api/threads/{id}/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.puffle.ai/api/threads/{id}/messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'body_text' => '<string>',
'body_html' => '<string>',
'subject' => '<string>',
'account_id' => '<string>',
'cc' => '<string>',
'bcc' => '<string>',
'attachments' => [
[
'name' => '<string>',
'bucket' => '<string>',
'path' => '<string>',
'url' => '<string>',
'mime_type' => '<string>',
'size' => 1,
'content_id' => '<string>'
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.puffle.ai/api/threads/{id}/messages"
payload := strings.NewReader("{\n \"body_text\": \"<string>\",\n \"body_html\": \"<string>\",\n \"subject\": \"<string>\",\n \"account_id\": \"<string>\",\n \"cc\": \"<string>\",\n \"bcc\": \"<string>\",\n \"attachments\": [\n {\n \"name\": \"<string>\",\n \"bucket\": \"<string>\",\n \"path\": \"<string>\",\n \"url\": \"<string>\",\n \"mime_type\": \"<string>\",\n \"size\": 1,\n \"content_id\": \"<string>\"\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.puffle.ai/api/threads/{id}/messages")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"body_text\": \"<string>\",\n \"body_html\": \"<string>\",\n \"subject\": \"<string>\",\n \"account_id\": \"<string>\",\n \"cc\": \"<string>\",\n \"bcc\": \"<string>\",\n \"attachments\": [\n {\n \"name\": \"<string>\",\n \"bucket\": \"<string>\",\n \"path\": \"<string>\",\n \"url\": \"<string>\",\n \"mime_type\": \"<string>\",\n \"size\": 1,\n \"content_id\": \"<string>\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.puffle.ai/api/threads/{id}/messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"body_text\": \"<string>\",\n \"body_html\": \"<string>\",\n \"subject\": \"<string>\",\n \"account_id\": \"<string>\",\n \"cc\": \"<string>\",\n \"bcc\": \"<string>\",\n \"attachments\": [\n {\n \"name\": \"<string>\",\n \"bucket\": \"<string>\",\n \"path\": \"<string>\",\n \"url\": \"<string>\",\n \"mime_type\": \"<string>\",\n \"size\": 1,\n \"content_id\": \"<string>\"\n }\n ]\n}"
response = http.request(request)
puts response.read_bodyUnibox
Send Message
Send an email or LinkedIn message in an existing Unibox thread.
POST
/
api
/
threads
/
{id}
/
messages
Send Message
curl --request POST \
--url https://app.puffle.ai/api/threads/{id}/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"body_text": "<string>",
"body_html": "<string>",
"subject": "<string>",
"account_id": "<string>",
"cc": "<string>",
"bcc": "<string>",
"attachments": [
{
"name": "<string>",
"bucket": "<string>",
"path": "<string>",
"url": "<string>",
"mime_type": "<string>",
"size": 1,
"content_id": "<string>"
}
]
}
'import requests
url = "https://app.puffle.ai/api/threads/{id}/messages"
payload = {
"body_text": "<string>",
"body_html": "<string>",
"subject": "<string>",
"account_id": "<string>",
"cc": "<string>",
"bcc": "<string>",
"attachments": [
{
"name": "<string>",
"bucket": "<string>",
"path": "<string>",
"url": "<string>",
"mime_type": "<string>",
"size": 1,
"content_id": "<string>"
}
]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
body_text: '<string>',
body_html: '<string>',
subject: '<string>',
account_id: '<string>',
cc: '<string>',
bcc: '<string>',
attachments: [
{
name: '<string>',
bucket: '<string>',
path: '<string>',
url: '<string>',
mime_type: '<string>',
size: 1,
content_id: '<string>'
}
]
})
};
fetch('https://app.puffle.ai/api/threads/{id}/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.puffle.ai/api/threads/{id}/messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'body_text' => '<string>',
'body_html' => '<string>',
'subject' => '<string>',
'account_id' => '<string>',
'cc' => '<string>',
'bcc' => '<string>',
'attachments' => [
[
'name' => '<string>',
'bucket' => '<string>',
'path' => '<string>',
'url' => '<string>',
'mime_type' => '<string>',
'size' => 1,
'content_id' => '<string>'
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.puffle.ai/api/threads/{id}/messages"
payload := strings.NewReader("{\n \"body_text\": \"<string>\",\n \"body_html\": \"<string>\",\n \"subject\": \"<string>\",\n \"account_id\": \"<string>\",\n \"cc\": \"<string>\",\n \"bcc\": \"<string>\",\n \"attachments\": [\n {\n \"name\": \"<string>\",\n \"bucket\": \"<string>\",\n \"path\": \"<string>\",\n \"url\": \"<string>\",\n \"mime_type\": \"<string>\",\n \"size\": 1,\n \"content_id\": \"<string>\"\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.puffle.ai/api/threads/{id}/messages")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"body_text\": \"<string>\",\n \"body_html\": \"<string>\",\n \"subject\": \"<string>\",\n \"account_id\": \"<string>\",\n \"cc\": \"<string>\",\n \"bcc\": \"<string>\",\n \"attachments\": [\n {\n \"name\": \"<string>\",\n \"bucket\": \"<string>\",\n \"path\": \"<string>\",\n \"url\": \"<string>\",\n \"mime_type\": \"<string>\",\n \"size\": 1,\n \"content_id\": \"<string>\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.puffle.ai/api/threads/{id}/messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"body_text\": \"<string>\",\n \"body_html\": \"<string>\",\n \"subject\": \"<string>\",\n \"account_id\": \"<string>\",\n \"cc\": \"<string>\",\n \"bcc\": \"<string>\",\n \"attachments\": [\n {\n \"name\": \"<string>\",\n \"bucket\": \"<string>\",\n \"path\": \"<string>\",\n \"url\": \"<string>\",\n \"mime_type\": \"<string>\",\n \"size\": 1,\n \"content_id\": \"<string>\"\n }\n ]\n}"
response = http.request(request)
puts response.read_bodyCLI:
Attachment uploads are available only to authenticated dashboard sessions. Public API and CLI callers must omit
On success, the response includes the saved outbound
puffle thread message send --id <id> --body-text <body-text>
puffle thread message send --id <id> --body-text <body-text> --body-html <body-html> --subject <subject> --account-id <account-id> --cc <cc> --bcc <bcc> --threading-mode <threading-mode>
Overview
Sends a message in an existing Unibox thread. For a new one-off email, first call Create thread, then call this endpoint withthreading_mode: "new_thread". For an existing email or LinkedIn conversation, call this endpoint on the existing thread to reply from the connected sender.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Thread ID returned by POST /api/threads or any thread listing endpoint. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
body_text | string | Yes | Plain-text message body. Empty strings are rejected. |
body_html | string | Optional | HTML body. For email, if omitted, Puffle derives HTML from body_text. |
subject | string | Required for new email threads unless the thread already has a subject | Email subject. Ignored by LinkedIn sends. |
account_id | string | Required for email when the thread has no sender | Email sender account ID. The account must belong to the workspace and be ready to send. |
threading_mode | "new_thread" or "reply" | Optional | Email threading behavior. Use "new_thread" for a one-off outbound email. If omitted, Puffle replies to the most recent inbound email when possible. |
cc | string | Optional | Comma-separated CC recipients for email. |
bcc | string | Optional | Comma-separated BCC recipients for email. |
attachments | array | Dashboard only | Attachment metadata uploaded through the dashboard. Bearer API-key requests that include this field return 403. |
attachments.
Send a One-Off Email
curl -X POST "https://app.puffle.ai/api/threads/3d986dd1-9d70-45b7-8e36-5d5b8b1c9337/messages" \
-H "Authorization: Bearer $PUFFLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_id": "8a2f0c7a-9d1e-4d8f-9d7d-6a8b8e8f4a11",
"subject": "Quick question",
"body_text": "Hi Alex, I had a quick question about your outbound workflow.",
"threading_mode": "new_thread"
}'
message and the refreshed thread. A one-off outbound email appears in Unibox with thread.inbox_state set to sent_one_off.
{
"message": {
"id": "6d54211c-7e24-4eb9-85fc-7388c8d89f2f",
"conversation_id": "3d986dd1-9d70-45b7-8e36-5d5b8b1c9337",
"direction": "outbound",
"subject": "Quick question",
"body_text": "Hi Alex, I had a quick question about your outbound workflow."
},
"thread": {
"id": "3d986dd1-9d70-45b7-8e36-5d5b8b1c9337",
"channel": "email",
"status": "active",
"inbox_state": "sent_one_off",
"outbound_message_count": 1,
"has_one_off_message": true
}
}
Error Notes
| Status | When it happens |
|---|---|
400 | body_text is missing or empty, threading_mode is invalid, or attachment metadata is invalid. |
403 | An API-key request includes dashboard-only attachments. |
404 | The thread does not exist or does not belong to the workspace. |
422 | The thread cannot send on the requested channel, the recipient email is missing, or the sender account is not ready. |
429 | The email provider rate-limited the send. |
500 | The message may have been sent externally but failed to save locally. If the response says the message was sent but could not be saved, do not retry; the recipient may already have received it. |
502 | The provider failed to send the message. |
AI agent notes
For a brand-new one-off email, create the thread first and send withthreading_mode: "new_thread". Do not retry a 500 response that says the message was sent but could not be saved, because the recipient may already have received it.Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Path Parameters
Thread (conversations.id) UUID.
Body
application/json
Plain-text message body.
Minimum string length:
1Optional HTML message body.
Email subject.
Email sender account ID.
Email CC recipients.
Email BCC recipients.
Email threading behavior.
Available options:
new_thread, reply Uploaded attachment metadata.
Show child attributes
Show child attributes
Response
Successful response.
Was this page helpful?
⌘I