Create Thread
curl --request POST \
--url https://app.puffle.ai/api/threads \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"account_id": "<string>",
"participant_email": "<string>",
"participant_name": "<string>",
"draft_subject": "<string>",
"draft_body_text": "<string>",
"draft_body_html": "<string>",
"draft_cc": "<string>",
"draft_bcc": "<string>",
"draft_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"
payload = {
"account_id": "<string>",
"participant_email": "<string>",
"participant_name": "<string>",
"draft_subject": "<string>",
"draft_body_text": "<string>",
"draft_body_html": "<string>",
"draft_cc": "<string>",
"draft_bcc": "<string>",
"draft_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({
account_id: '<string>',
participant_email: '<string>',
participant_name: '<string>',
draft_subject: '<string>',
draft_body_text: '<string>',
draft_body_html: '<string>',
draft_cc: '<string>',
draft_bcc: '<string>',
draft_attachments: [
{
name: '<string>',
bucket: '<string>',
path: '<string>',
url: '<string>',
mime_type: '<string>',
size: 1,
content_id: '<string>'
}
]
})
};
fetch('https://app.puffle.ai/api/threads', 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",
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([
'account_id' => '<string>',
'participant_email' => '<string>',
'participant_name' => '<string>',
'draft_subject' => '<string>',
'draft_body_text' => '<string>',
'draft_body_html' => '<string>',
'draft_cc' => '<string>',
'draft_bcc' => '<string>',
'draft_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"
payload := strings.NewReader("{\n \"account_id\": \"<string>\",\n \"participant_email\": \"<string>\",\n \"participant_name\": \"<string>\",\n \"draft_subject\": \"<string>\",\n \"draft_body_text\": \"<string>\",\n \"draft_body_html\": \"<string>\",\n \"draft_cc\": \"<string>\",\n \"draft_bcc\": \"<string>\",\n \"draft_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")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"account_id\": \"<string>\",\n \"participant_email\": \"<string>\",\n \"participant_name\": \"<string>\",\n \"draft_subject\": \"<string>\",\n \"draft_body_text\": \"<string>\",\n \"draft_body_html\": \"<string>\",\n \"draft_cc\": \"<string>\",\n \"draft_bcc\": \"<string>\",\n \"draft_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")
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 \"account_id\": \"<string>\",\n \"participant_email\": \"<string>\",\n \"participant_name\": \"<string>\",\n \"draft_subject\": \"<string>\",\n \"draft_body_text\": \"<string>\",\n \"draft_body_html\": \"<string>\",\n \"draft_cc\": \"<string>\",\n \"draft_bcc\": \"<string>\",\n \"draft_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
Create Thread
Create a draft one-off email thread in Unibox.
POST
/
api
/
threads
Create Thread
curl --request POST \
--url https://app.puffle.ai/api/threads \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"account_id": "<string>",
"participant_email": "<string>",
"participant_name": "<string>",
"draft_subject": "<string>",
"draft_body_text": "<string>",
"draft_body_html": "<string>",
"draft_cc": "<string>",
"draft_bcc": "<string>",
"draft_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"
payload = {
"account_id": "<string>",
"participant_email": "<string>",
"participant_name": "<string>",
"draft_subject": "<string>",
"draft_body_text": "<string>",
"draft_body_html": "<string>",
"draft_cc": "<string>",
"draft_bcc": "<string>",
"draft_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({
account_id: '<string>',
participant_email: '<string>',
participant_name: '<string>',
draft_subject: '<string>',
draft_body_text: '<string>',
draft_body_html: '<string>',
draft_cc: '<string>',
draft_bcc: '<string>',
draft_attachments: [
{
name: '<string>',
bucket: '<string>',
path: '<string>',
url: '<string>',
mime_type: '<string>',
size: 1,
content_id: '<string>'
}
]
})
};
fetch('https://app.puffle.ai/api/threads', 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",
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([
'account_id' => '<string>',
'participant_email' => '<string>',
'participant_name' => '<string>',
'draft_subject' => '<string>',
'draft_body_text' => '<string>',
'draft_body_html' => '<string>',
'draft_cc' => '<string>',
'draft_bcc' => '<string>',
'draft_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"
payload := strings.NewReader("{\n \"account_id\": \"<string>\",\n \"participant_email\": \"<string>\",\n \"participant_name\": \"<string>\",\n \"draft_subject\": \"<string>\",\n \"draft_body_text\": \"<string>\",\n \"draft_body_html\": \"<string>\",\n \"draft_cc\": \"<string>\",\n \"draft_bcc\": \"<string>\",\n \"draft_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")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"account_id\": \"<string>\",\n \"participant_email\": \"<string>\",\n \"participant_name\": \"<string>\",\n \"draft_subject\": \"<string>\",\n \"draft_body_text\": \"<string>\",\n \"draft_body_html\": \"<string>\",\n \"draft_cc\": \"<string>\",\n \"draft_bcc\": \"<string>\",\n \"draft_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")
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 \"account_id\": \"<string>\",\n \"participant_email\": \"<string>\",\n \"participant_name\": \"<string>\",\n \"draft_subject\": \"<string>\",\n \"draft_body_text\": \"<string>\",\n \"draft_body_html\": \"<string>\",\n \"draft_cc\": \"<string>\",\n \"draft_bcc\": \"<string>\",\n \"draft_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:
At least one draft field must be present. A sendable one-off email needs
The response includes the created draft as
puffle thread create --account-id <account-id> --participant-email <participant-email> --participant-name <participant-name> --draft-subject <draft-subject> --draft-body-text <draft-body-text> --draft-body-html <draft-body-html> --draft-cc <draft-cc> --draft-bcc <draft-bcc>
Overview
Creates a draft one-off email thread in Unibox. This does not send the email by itself. To send the message, call Send message with the returnedthread.id.
Use this endpoint when you want to start a new direct email conversation outside a campaign. Campaign sends still belong to the Campaigns API and sequence engine.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
participant_email | string | Required to send | Recipient email address. The API stores it lowercase. |
account_id | string | Optional | Email sender account ID. If provided, it must belong to the workspace, be an email account, have a configured sending inbox, and be ready to send. You may also provide this later when sending. |
participant_name | string | Optional | Display name for the recipient. |
draft_subject | string | Optional | Draft subject stored on the thread. |
draft_body_text | string | Optional | Plain-text draft body stored on the thread. |
draft_body_html | string | Optional | HTML draft body stored on the thread. |
draft_cc | string | Optional | Comma-separated CC recipients for the draft. |
draft_bcc | string | Optional | Comma-separated BCC recipients for the draft. |
draft_attachments | array | Forbidden when non-empty | Do not attach files while creating a thread. A non-empty array returns 403; create the draft first, then add attachments in Unibox. |
participant_email plus an email sender account, either here as account_id or later in the send request.
Attachment upload metadata is available only to authenticated dashboard sessions. No caller can attach files while creating a thread: omit draft_attachments or send the dashboard’s empty array, create the draft first, then add attachments to it in Unibox. Both Bearer API-key and session requests return 403 for a non-empty array.
Example
curl -X POST "https://app.puffle.ai/api/threads" \
-H "Authorization: Bearer $PUFFLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_id": "8a2f0c7a-9d1e-4d8f-9d7d-6a8b8e8f4a11",
"participant_email": "founder@example.com",
"participant_name": "Alex Founder",
"draft_subject": "Quick question",
"draft_body_text": "Hi Alex, I had a quick question about your outbound workflow."
}'
thread.
{
"thread": {
"id": "3d986dd1-9d70-45b7-8e36-5d5b8b1c9337",
"channel": "email",
"status": "draft",
"account_id": "8a2f0c7a-9d1e-4d8f-9d7d-6a8b8e8f4a11",
"participant_email": "founder@example.com",
"draft_subject": "Quick question",
"draft_body_text": "Hi Alex, I had a quick question about your outbound workflow."
},
"existing_threads": {
"exists": false,
"total": 0,
"threads": []
}
}
Duplicate Recipient Handling
If the same sender already has a non-archived, non-warmup email thread with the recipient, the API returns409 with existing_thread and existing_threads. Open that thread and call Send message instead of creating a duplicate.
AI agent notes
Use this endpoint beforePOST /api/threads/{id}/messages when starting a one-off email. Do not use it to send campaign sequence messages.Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
application/json
Email sender account ID, or null when no sender is selected yet.
Recipient email address.
Recipient display name.
Draft email subject.
Draft plain-text body.
Draft HTML body.
Draft CC recipients.
Draft BCC recipients.
Forbidden when non-empty for all callers. Omit this field or send an empty array, then add attachments after creating the thread in Unibox.
Show child attributes
Show child attributes
Response
Thread created.
Was this page helpful?
⌘I