Introduction
This documentation aims to provide all the information you need to work with our API.
Authenticating requests
To authenticate requests, include an Authorization header with the value "Bearer {YOUR_API_KEY}".
All authenticated endpoints are marked with a requires authentication badge in the documentation below.
You can retrieve your token by visiting your Team's dashboard and clicking Generate API token.
Contact Fields
APIs for managing contact fields.
Contact fields define the data structure for your contacts. Each team has its own set of fields that determine what information can be stored on contacts.
Field Types
| Type | Description | Example Value |
|---|---|---|
text |
Plain text string | "John Doe" |
email |
Email address (validated) | "john@example.com" |
phone |
Phone number (normalized to E.164) | "+14155552671" |
number |
Numeric value | 42 or 3.14 |
date |
Date value | "2024-01-15" |
datetime |
Date and time | "2024-01-15T10:30:00Z" |
boolean |
True/false value | true or false |
url |
Web URL | "https://example.com" |
pick_list |
Selection from predefined options | "option_a" |
related |
Reference to another contact | Contact ID |
Field Tags (Merge Tags)
Each field has a unique tag identifier (also called a merge tag) that you use when:
- Creating or updating contacts via the API
- Personalizing email templates with contact data
- Searching for contacts by field values
Tags should be lowercase, use underscores for spaces, and be descriptive (e.g., first_name, company_name, membership_level).
Contact Types
Fields are scoped to a contact type:
person- Fields for individual contactsorganization- Fields for company/organization contacts
Integration Fields
Some fields may be created by third-party integrations (like ChamberMaster or Mailchimp).
These fields have an integration_id set and cannot be modified or deleted through the API.
List all contact fields
requires authentication
Get all contact fields for your team. Use the contact_type query parameter to filter
by person or organization fields.
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/contact-fields?contact_type=person" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/contact-fields"
);
const params = {
"contact_type": "person",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contact-fields'
params = {
'contact_type': 'person',
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, params=params)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/contact-fields',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'contact_type' => 'person',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contact-fields?contact_type=person"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
[
{
"id": 1,
"name": "First Name",
"tag": "first_name",
"type": "text",
"contact_type": "person",
"max_length": 255,
"integration_id": null
},
{
"id": 2,
"name": "Email",
"tag": "email",
"type": "email",
"contact_type": "person",
"max_length": 255,
"integration_id": null
}
]
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get available field types
requires authentication
Returns a list of all available contact field types with their display names.
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/contact-fields/types" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/contact-fields/types"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contact-fields/types'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/contact-fields/types',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contact-fields/types"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
[
{
"value": "text",
"name": "Text"
},
{
"value": "email",
"name": "Email"
},
{
"value": "phone",
"name": "Phone"
},
{
"value": "number",
"name": "Number"
},
{
"value": "date",
"name": "Date"
},
{
"value": "datetime",
"name": "Datetime"
},
{
"value": "boolean",
"name": "True / False"
},
{
"value": "url",
"name": "URL"
},
{
"value": "pick_list",
"name": "Pick List"
},
{
"value": "related",
"name": "Related Contact/Organization"
}
]
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create a contact field
requires authentication
Create a new contact field for your team.
Example request:
curl --request POST \
"https://www.sallyjo.com/api/v1/contact-fields" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Membership Level\",
\"tag\": \"membership_level\",
\"type\": \"pick_list\",
\"contact_type\": \"person\",
\"max_length\": 100,
\"options\": [
\"gold\",
\"silver\",
\"bronze\"
],
\"allow_multiple\": false
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/contact-fields"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Membership Level",
"tag": "membership_level",
"type": "pick_list",
"contact_type": "person",
"max_length": 100,
"options": [
"gold",
"silver",
"bronze"
],
"allow_multiple": false
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contact-fields'
payload = {
"name": "Membership Level",
"tag": "membership_level",
"type": "pick_list",
"contact_type": "person",
"max_length": 100,
"options": [
"gold",
"silver",
"bronze"
],
"allow_multiple": false
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->post(
'https://www.sallyjo.com/api/v1/contact-fields',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Membership Level',
'tag' => 'membership_level',
'type' => 'pick_list',
'contact_type' => 'person',
'max_length' => 100,
'options' => [
'gold',
'silver',
'bronze',
],
'allow_multiple' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("Membership Level"), "name");
data.Add(new StringContent("membership_level"), "tag");
data.Add(new StringContent("pick_list"), "type");
data.Add(new StringContent("person"), "contact_type");
data.Add(new StringContent("100"), "max_length");
data.Add(new StringContent("gold"), "options[]");
data.Add(new StringContent(""), "allow_multiple");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contact-fields"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (201):
{
"id": 10,
"name": "Membership Level",
"tag": "membership_level",
"type": "pick_list",
"contact_type": "person",
"max_length": null,
"options": [
"gold",
"silver",
"bronze"
],
"allow_multiple": false,
"integration_id": null
}
Example response (422):
{
"message": "The tag has already been taken."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get a contact field
requires authentication
Retrieve details for a specific contact field.
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/contact-fields/8" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/contact-fields/8"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contact-fields/8'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/contact-fields/8',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contact-fields/8"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
{
"id": 1,
"name": "First Name",
"tag": "first_name",
"type": "text",
"contact_type": "person",
"max_length": 255,
"options": null,
"allow_multiple": false,
"integration_id": null
}
Example response (403):
{
"message": "This action is unauthorized."
}
Example response (404):
{
"message": "No query results for model [ContactField]"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update a contact field
requires authentication
Update an existing contact field. Integration fields cannot be updated.
Example request:
curl --request PUT \
"https://www.sallyjo.com/api/v1/contact-fields/16" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Full Name\",
\"tag\": \"full_name\",
\"type\": \"text\",
\"max_length\": 150,
\"options\": [
\"premium\",
\"standard\",
\"basic\"
],
\"allow_multiple\": true
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/contact-fields/16"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Full Name",
"tag": "full_name",
"type": "text",
"max_length": 150,
"options": [
"premium",
"standard",
"basic"
],
"allow_multiple": true
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contact-fields/16'
payload = {
"name": "Full Name",
"tag": "full_name",
"type": "text",
"max_length": 150,
"options": [
"premium",
"standard",
"basic"
],
"allow_multiple": true
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('PUT', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->put(
'https://www.sallyjo.com/api/v1/contact-fields/16',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Full Name',
'tag' => 'full_name',
'type' => 'text',
'max_length' => 150,
'options' => [
'premium',
'standard',
'basic',
],
'allow_multiple' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("Full Name"), "name");
data.Add(new StringContent("full_name"), "tag");
data.Add(new StringContent("text"), "type");
data.Add(new StringContent("150"), "max_length");
data.Add(new StringContent("premium"), "options[]");
data.Add(new StringContent("1"), "allow_multiple");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contact-fields/16"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
{
"id": 1,
"name": "Full Name",
"tag": "full_name",
"type": "text",
"contact_type": "person",
"max_length": 150,
"options": null,
"allow_multiple": false,
"integration_id": null
}
Example response (403):
{
"message": "This field is managed by an integration and cannot be modified."
}
Example response (422):
{
"message": "The tag has already been taken."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Delete a contact field
requires authentication
Delete a contact field. Integration fields cannot be deleted.
Warning: Deleting a field will remove the field definition, but existing contact data stored in that field will remain in the database (orphaned). Consider exporting contact data before deleting fields.
Example request:
curl --request DELETE \
"https://www.sallyjo.com/api/v1/contact-fields/20" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/contact-fields/20"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contact-fields/20'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('DELETE', url, headers=headers)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->delete(
'https://www.sallyjo.com/api/v1/contact-fields/20',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contact-fields/20"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
{
"message": "Contact field deleted successfully."
}
Example response (403):
{
"message": "This field is managed by an integration and cannot be deleted."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Contact management
APIs for managing contacts.
Understanding Contact Fields
Contacts in SallyJo use a flexible, team-specific field system. Each team can define their own custom fields (like "first_name", "company", "membership_level", etc.) to store contact data.
How Contact Fields Work
- Dynamic Fields: Each team has its own set of contact fields. Use the Contact Fields API to list your team's available fields.
- Field Tags: When creating or updating contacts, use the field's
tag(merge tag identifier) as the key. For example, if you have a field with tagfirst_name, pass{"first_name": "John"}. - Field Types: Fields can be
text,email,phone,number,date,datetime,boolean,url,pick_list, orrelated. - Contact Types: Fields are scoped to either "person" or "organization" contact types. A "person" contact uses person fields, while an "organization" uses organization fields.
Example Workflow
- First, fetch your team's contact fields using
GET /api/v1/contact-fields - Use the field
tagvalues as keys when creating/updating contacts - The field
typetells you what data format to use (e.g., email fields expect valid email addresses)
Note: The bodyParam examples below use default team fields (first_name, last_name, email, phone, address).
Your team may have different or additional custom fields. Always check GET /api/v1/contact-fields for your actual fields.
Get contact list
requires authentication
Returns a paginated list of contacts belonging to the authenticated
team. Response shape matches GET /api/v1/contacts/search — the
data[] array is transformed via Contact::jsonSerialize() so each
row exposes both raw attributes (keyed by field id) and flattened
field-tag keys (e.g. first_name, email) alongside the base model
columns.
For filtered / searchable results use GET /api/v1/contacts/search.
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/contacts?page=1&per_page=15" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"page\": 3,
\"per_page\": 19
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/contacts"
);
const params = {
"page": "1",
"per_page": "15",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"page": 3,
"per_page": 19
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contacts'
payload = {
"page": 3,
"per_page": 19
}
params = {
'page': '1',
'per_page': '15',
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, json=payload, params=params)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/contacts',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'page' => '1',
'per_page' => '15',
],
'json' => [
'page' => 3,
'per_page' => 19,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contacts?page=1&per_page=15"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200, Success):
{
"current_page": 1,
"data": [
{
"id": 1,
"team_id": 1,
"type": "person",
"attributes": {
"1": "John",
"2": "Doe",
"3": "john@example.com"
},
"uuid_id": "01H8ZM6E4Q1A8X7ZG9K2P5S3RT",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"tags": [
{
"slug": "customer",
"name": "Customer"
}
],
"created_at": "2024-06-01T10:30:00.000000Z",
"updated_at": "2026-01-15T14:45:00.000000Z"
}
],
"per_page": 15,
"total": 42,
"last_page": 3,
"first_page_url": "https://app.sallyjo.com/api/v1/contacts?page=1",
"last_page_url": "https://app.sallyjo.com/api/v1/contacts?page=3",
"next_page_url": "https://app.sallyjo.com/api/v1/contacts?page=2",
"prev_page_url": null,
"path": "https://app.sallyjo.com/api/v1/contacts",
"from": 1,
"to": 15
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (422, Invalid pagination parameters):
{
"message": "The per page field must not be greater than 100.",
"errors": {
"per_page": [
"The per page field must not be greater than 100."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Search for a contact
requires authentication
Filters the team's contacts by any combination of built-in fields
(order_by, page, per_page, tags, searchTerm) and
arbitrary contact-field tags — any key that matches a tag from
GET /api/v1/contact-fields is treated as an exact-value filter on
that field's JSON attribute. Unknown keys return 422.
Special filters
searchTerm— space-separated tokens, each substring-matched against the JSON blob of contact attributes (case-insensitive).tags— array of tag slugs; contacts must have ALL supplied tags.- Email/phone field values are normalized before comparison (E.164 for phones, RFC-clean for emails).
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/contacts/search?order_by=last_name&page=1&per_page=15&searchTerm=john+doe&tags[]=customer&tags[]=vip&first_name=John" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/contacts/search"
);
const params = {
"order_by": "last_name",
"page": "1",
"per_page": "15",
"searchTerm": "john doe",
"tags[0]": "customer",
"tags[1]": "vip",
"first_name": "John",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contacts/search'
params = {
'order_by': 'last_name',
'page': '1',
'per_page': '15',
'searchTerm': 'john doe',
'tags[0]': 'customer',
'tags[1]': 'vip',
'first_name': 'John',
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, params=params)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/contacts/search',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'order_by' => 'last_name',
'page' => '1',
'per_page' => '15',
'searchTerm' => 'john doe',
'tags[0]' => 'customer',
'tags[1]' => 'vip',
'first_name' => 'John',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contacts/search?order_by=last_name&page=1&per_page=15&searchTerm=john+doe&tags[]=customer&tags[]=vip&first_name=John"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200, Success):
{
"current_page": 1,
"data": [
{
"id": 1,
"team_id": 1,
"type": "person",
"attributes": {
"1": "John",
"2": "Doe",
"3": "john@example.com"
},
"uuid_id": "01H8ZM6E4Q1A8X7ZG9K2P5S3RT"
}
],
"per_page": 15,
"total": 1,
"last_page": 1
}
Example response (422, Unknown field tag supplied):
{
"message": "The unknown_field filter is not supported for this team.",
"errors": {
"unknown_field": [
"The unknown_field filter is not supported for this team."
]
}
}
Example response (422, Invalid email in email-typed field):
{
"message": "The email must be a valid email address.",
"errors": {
"email": [
"The email must be a valid email address."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create a new contact
requires authentication
This endpoint allows you to create a new contact. Contact fields are team-specific and use the field's "tag" (merge tag identifier) as the key.
Example request:
curl --request POST \
"https://www.sallyjo.com/api/v1/contacts/create" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"type\": \"person\",
\"first_name\": \"John\",
\"last_name\": \"Doe\",
\"email\": \"john@example.com\",
\"phone\": \"+14155552671\",
\"birthday\": \"1990-05-15\",
\"address\": \"123 Main Street\",
\"city\": \"San Francisco\",
\"state\": \"CA\",
\"zipcode\": \"94102\",
\"tags\": [
\"customer\",
\"vip\"
]
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/contacts/create"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"type": "person",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "+14155552671",
"birthday": "1990-05-15",
"address": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"zipcode": "94102",
"tags": [
"customer",
"vip"
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contacts/create'
payload = {
"type": "person",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "+14155552671",
"birthday": "1990-05-15",
"address": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"zipcode": "94102",
"tags": [
"customer",
"vip"
]
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->post(
'https://www.sallyjo.com/api/v1/contacts/create',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'type' => 'person',
'first_name' => 'John',
'last_name' => 'Doe',
'email' => 'john@example.com',
'phone' => '+14155552671',
'birthday' => '1990-05-15',
'address' => '123 Main Street',
'city' => 'San Francisco',
'state' => 'CA',
'zipcode' => '94102',
'tags' => [
'customer',
'vip',
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("person"), "type");
data.Add(new StringContent("John"), "first_name");
data.Add(new StringContent("Doe"), "last_name");
data.Add(new StringContent("john@example.com"), "email");
data.Add(new StringContent("+14155552671"), "phone");
data.Add(new StringContent("1990-05-15"), "birthday");
data.Add(new StringContent("123 Main Street"), "address");
data.Add(new StringContent("San Francisco"), "city");
data.Add(new StringContent("CA"), "state");
data.Add(new StringContent("94102"), "zipcode");
data.Add(new StringContent("customer"), "tags[]");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contacts/create"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
{
"id": 1,
"team_id": 1,
"type": "person",
"attributes": {
"1": "John",
"2": "Doe",
"3": "john@example.com"
},
"tags": [
{
"id": 1,
"slug": "customer"
}
]
}
Example response (403):
{
"message": "This action is unauthorized."
}
Example response (422):
{
"message": "Tags not found: [found:1][passed:2][missing:invalid-tag]"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get a contact by id
requires authentication
Returns a single contact by ID. The contact must belong to the authenticated team or the request is rejected with 403.
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/contacts/11" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/contacts/11"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contacts/11'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/contacts/11',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contacts/11"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200, Success):
{
"id": 1,
"team_id": 1,
"type": "person",
"attributes": {
"1": "John",
"2": "Doe",
"3": "john@example.com"
},
"uuid_id": "01H8ZM6E4Q1A8X7ZG9K2P5S3RT",
"created_at": "2024-06-01T10:30:00.000000Z",
"updated_at": "2026-01-15T14:45:00.000000Z"
}
Example response (403, Contact belongs to another team):
{
"message": "This action is unauthorized."
}
Example response (404, Contact not found):
{
"message": "No query results for model [Contact]."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update an existing contact
requires authentication
This endpoint allows you to update an existing contact. Contact fields are team-specific and use the field's "tag" (merge tag identifier) as the key.
Example request:
curl --request PUT \
"https://www.sallyjo.com/api/v1/contacts/19/update" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"first_name\": \"Jane\",
\"last_name\": \"Smith\",
\"email\": \"jane@example.com\",
\"phone\": \"+14155552672\",
\"birthday\": \"1985-08-22\",
\"address\": \"456 Oak Avenue\",
\"city\": \"Los Angeles\",
\"state\": \"CA\",
\"zipcode\": \"90210\",
\"tags\": [
\"customer\",
\"newsletter\"
]
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/contacts/19/update"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"phone": "+14155552672",
"birthday": "1985-08-22",
"address": "456 Oak Avenue",
"city": "Los Angeles",
"state": "CA",
"zipcode": "90210",
"tags": [
"customer",
"newsletter"
]
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contacts/19/update'
payload = {
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"phone": "+14155552672",
"birthday": "1985-08-22",
"address": "456 Oak Avenue",
"city": "Los Angeles",
"state": "CA",
"zipcode": "90210",
"tags": [
"customer",
"newsletter"
]
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('PUT', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->put(
'https://www.sallyjo.com/api/v1/contacts/19/update',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'first_name' => 'Jane',
'last_name' => 'Smith',
'email' => 'jane@example.com',
'phone' => '+14155552672',
'birthday' => '1985-08-22',
'address' => '456 Oak Avenue',
'city' => 'Los Angeles',
'state' => 'CA',
'zipcode' => '90210',
'tags' => [
'customer',
'newsletter',
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("Jane"), "first_name");
data.Add(new StringContent("Smith"), "last_name");
data.Add(new StringContent("jane@example.com"), "email");
data.Add(new StringContent("+14155552672"), "phone");
data.Add(new StringContent("1985-08-22"), "birthday");
data.Add(new StringContent("456 Oak Avenue"), "address");
data.Add(new StringContent("Los Angeles"), "city");
data.Add(new StringContent("CA"), "state");
data.Add(new StringContent("90210"), "zipcode");
data.Add(new StringContent("customer"), "tags[]");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contacts/19/update"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
{
"id": 1,
"team_id": 1,
"type": "person",
"attributes": {
"1": "Jane",
"2": "Smith",
"3": "jane@example.com"
},
"tags": [
{
"id": 1,
"slug": "customer"
}
]
}
Example response (403):
{
"message": "This action is unauthorized."
}
Example response (422):
{
"message": "Tags not found: [found:1][passed:2][missing:invalid-tag]"
}
Example response (422):
{
"message": "Field with tag 'unknown_field' not found"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Delete a contact
requires authentication
Permanently deletes a contact and all associated CRM data (notes,
tasks, tag assignments). Subscription rows are cascaded via the
database. Returns true on success. The contact must belong to the
authenticated team or the request is rejected with 403.
Example request:
curl --request DELETE \
"https://www.sallyjo.com/api/v1/contacts/9" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/contacts/9"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/contacts/9'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('DELETE', url, headers=headers)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->delete(
'https://www.sallyjo.com/api/v1/contacts/9',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/contacts/9"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200, Success):
true
Example response (403, Contact belongs to another team):
{
"message": "This action is unauthorized."
}
Example response (404, Contact not found):
{
"message": "No query results for model [Contact]."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Email List management
APIs for managing email lists and subscriptions.
Get list subscribers
requires authentication
Get all subscriptions for an email list, with optional filtering.
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/lists/5/subscribers?limit=20&order_by=last_name&page=5&email=subscriber%40example.com&status=active" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/lists/5/subscribers"
);
const params = {
"limit": "20",
"order_by": "last_name",
"page": "5",
"email": "subscriber@example.com",
"status": "active",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/lists/5/subscribers'
params = {
'limit': '20',
'order_by': 'last_name',
'page': '5',
'email': 'subscriber@example.com',
'status': 'active',
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, params=params)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/lists/5/subscribers',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'limit' => '20',
'order_by' => 'last_name',
'page' => '5',
'email' => 'subscriber@example.com',
'status' => 'active',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/lists/5/subscribers?limit=20&order_by=last_name&page=5&email=subscriber%40example.com&status=active"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
[
{
"id": 1,
"email_address_id": 5,
"email_list_id": 1,
"contact_id": 123,
"status": "active",
"address": {
"id": 5,
"email": "subscriber@example.com"
}
}
]
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create a new Subscription
requires authentication
This endpoint allows you to create a new subscription to an email list.
Example request:
curl --request POST \
"https://www.sallyjo.com/api/v1/lists/8/subscribers?email=maximilian48%40example.com&contact_id=17&limit=8&order_by=last_name&page=17" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"email\": \"subscriber@example.com\",
\"contact_id\": 123
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/lists/8/subscribers"
);
const params = {
"email": "maximilian48@example.com",
"contact_id": "17",
"limit": "8",
"order_by": "last_name",
"page": "17",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"email": "subscriber@example.com",
"contact_id": 123
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/lists/8/subscribers'
payload = {
"email": "subscriber@example.com",
"contact_id": 123
}
params = {
'email': 'maximilian48@example.com',
'contact_id': '17',
'limit': '8',
'order_by': 'last_name',
'page': '17',
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload, params=params)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->post(
'https://www.sallyjo.com/api/v1/lists/8/subscribers',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'email' => 'maximilian48@example.com',
'contact_id' => '17',
'limit' => '8',
'order_by' => 'last_name',
'page' => '17',
],
'json' => [
'email' => 'subscriber@example.com',
'contact_id' => 123,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("subscriber@example.com"), "email");
data.Add(new StringContent("123"), "contact_id");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/lists/8/subscribers?email=maximilian48%40example.com&contact_id=17&limit=8&order_by=last_name&page=17"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
{
"id": 1,
"email_address_id": 5,
"email_list_id": 1,
"contact_id": 123,
"status": "active"
}
Example response (400):
{
"message": "Failed to process the email address.",
"error": "Invalid email format"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update a subscription
requires authentication
Update the status or contact association of an existing subscription.
Example request:
curl --request PUT \
"https://www.sallyjo.com/api/v1/subscriptions/11/update" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"contact_id\": 123,
\"status\": \"active\"
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/subscriptions/11/update"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"contact_id": 123,
"status": "active"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/subscriptions/11/update'
payload = {
"contact_id": 123,
"status": "active"
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('PUT', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->put(
'https://www.sallyjo.com/api/v1/subscriptions/11/update',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'contact_id' => 123,
'status' => 'active',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("123"), "contact_id");
data.Add(new StringContent("active"), "status");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/subscriptions/11/update"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
{
"id": 1,
"email_address_id": 5,
"email_list_id": 1,
"contact_id": 123,
"status": "active"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Email management
APIs for managing emails
Send an email
requires authentication
Sends a transactional email via the team's configured mailer (SES).
You must supply either raw content (html / plaintext / subject)
or a saved-message reference (message_id). Sending from an address
that is not on the team's verified identity list returns 422.
Content vs saved message
- Raw content: pass
subjectand at least one ofhtml/plaintext. The endpoint will create (or reuse) anEmail\Messagerecord before sending. - Saved message: pass
message_id.subject/html/plaintextare ignored.
Verified sender addresses are listed under
GET /api/v1/verified-identities/emails.
Example request:
curl --request POST \
"https://www.sallyjo.com/api/v1/email/send" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "to=subscriber@example.com"\
--form "from=hello@acme.com"\
--form "subject=Your weekly digest"\
--form "html=<h1>Hi!</h1><p>Welcome.</p>"\
--form "plaintext=et"\
--form "message_id=174"\
--form "merge_data={"first_name":"Alex","promo_code":"WELCOME10"}"\
--form "cc[]=copy@example.com"\
--form "bcc[]=archive@example.com"\
--form "campaign_id=12"\
--form "attachments[]=@/tmp/php5tesai8sf15j7p9YEc2" const url = new URL(
"https://www.sallyjo.com/api/v1/email/send"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('to', 'subscriber@example.com');
body.append('from', 'hello@acme.com');
body.append('subject', 'Your weekly digest');
body.append('html', '<h1>Hi!</h1><p>Welcome.</p>');
body.append('plaintext', 'et');
body.append('message_id', '174');
body.append('merge_data', '{"first_name":"Alex","promo_code":"WELCOME10"}');
body.append('cc[]', 'copy@example.com');
body.append('bcc[]', 'archive@example.com');
body.append('campaign_id', '12');
body.append('attachments[]', document.querySelector('input[name="attachments[]"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/email/send'
files = {
'to': (None, 'subscriber@example.com'),
'from': (None, 'hello@acme.com'),
'subject': (None, 'Your weekly digest'),
'html': (None, '<h1>Hi!</h1><p>Welcome.</p>'),
'plaintext': (None, 'et'),
'message_id': (None, '174'),
'merge_data': (None, '{"first_name":"Alex","promo_code":"WELCOME10"}'),
'cc[]': (None, 'copy@example.com'),
'bcc[]': (None, 'archive@example.com'),
'campaign_id': (None, '12'),
'attachments[]': open('/tmp/php5tesai8sf15j7p9YEc2', 'rb')}
payload = {
"to": "subscriber@example.com",
"from": "hello@acme.com",
"subject": "Your weekly digest",
"html": "<h1>Hi!<\/h1><p>Welcome.<\/p>",
"plaintext": "et",
"message_id": 174,
"merge_data": "{\"first_name\":\"Alex\",\"promo_code\":\"WELCOME10\"}",
"cc": [
"copy@example.com"
],
"bcc": [
"archive@example.com"
],
"campaign_id": 12
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'multipart/form-data',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, files=files)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->post(
'https://www.sallyjo.com/api/v1/email/send',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'to',
'contents' => 'subscriber@example.com'
],
[
'name' => 'from',
'contents' => 'hello@acme.com'
],
[
'name' => 'subject',
'contents' => 'Your weekly digest'
],
[
'name' => 'html',
'contents' => '<h1>Hi!</h1><p>Welcome.</p>'
],
[
'name' => 'plaintext',
'contents' => 'et'
],
[
'name' => 'message_id',
'contents' => '174'
],
[
'name' => 'merge_data',
'contents' => '{"first_name":"Alex","promo_code":"WELCOME10"}'
],
[
'name' => 'cc[]',
'contents' => 'copy@example.com'
],
[
'name' => 'bcc[]',
'contents' => 'archive@example.com'
],
[
'name' => 'campaign_id',
'contents' => '12'
],
[
'name' => 'attachments[]',
'contents' => fopen('/tmp/php5tesai8sf15j7p9YEc2', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("subscriber@example.com"), "to");
data.Add(new StringContent("hello@acme.com"), "from");
data.Add(new StringContent("Your weekly digest"), "subject");
data.Add(new StringContent("<h1>Hi!</h1><p>Welcome.</p>"), "html");
data.Add(new StringContent("et"), "plaintext");
data.Add(new StringContent("174"), "message_id");
data.Add(new StringContent("{"first_name":"Alex","promo_code":"WELCOME10"}"), "merge_data");
data.Add(new StringContent("copy@example.com"), "cc[]");
data.Add(new StringContent("archive@example.com"), "bcc[]");
data.Add(new StringContent("12"), "campaign_id");
var file = new ByteArrayContent(System.IO.File.ReadAllBytes("/tmp/php5tesai8sf15j7p9YEc2"));
data.Add(file, "attachments[]", "test.png");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/email/send"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200, Success — the saved Sent record):
{
"id": 98765,
"uuid_id": "01H8ZM6E4Q1A8X7ZG9K2P5S3RT",
"team_id": 1,
"email_message_id": 174,
"email_campaign_id": null,
"from_address_id": 5,
"email_subscription_id": null,
"service": "ses",
"status": null,
"created_at": "2026-07-15T16:20:00.000000Z"
}
Example response (404, message_id not found on team):
{
"message": "Message not found"
}
Example response (422, Sender not verified):
{
"message": "The from field must be a verified identity for this team.",
"errors": {
"from": [
"The from field must be a verified identity for this team."
]
}
}
Example response (422, Recipient invalid or banned):
{
"message": "The to field must be a valid, non-banned email address.",
"errors": {
"to": [
"The to field must be a valid, non-banned email address."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Import an HTML creative
requires authentication
Converts the supplied HTML into the editor's MJML + slate JSON
representation and creates a new email creative on the authenticated
team. If any rule marked required matches zero times the whole
import is rejected with 422 and nothing is persisted — iterate against
/import/dry-run first to make sure your rules match cleanly.
Rule shape
Each entry in rules[] looks like:
{
"id": "kebab-case-only",
"stage": "dom",
"required": true,
"max": 1,
"find": { "type": "xpath", "pattern": "//tr[.//p[contains(., 'Tell a Friend')]]" },
"action": { "type": "remove" }
}
See docs/html-import-transform-plan.md (§6.1) for the full field
catalog and imports/{main,second,whale}/rules.json for reference
rulesets. find.type may be css | xpath | regex | text; action.type
may be remove | replace | set-attr | remove-attr | set-text | wrap | unwrap | insert-adjacent.
Data mappings
The optional data_mappings[] payload wires merge-tag chips in the
inserted markup (e.g. {{ spotlight.title }}) to content-source
bindings resolved at send time. See docs/email-import-data-mappings.md
for the binding schema and namespace rules.
Example request:
curl --request POST \
"https://www.sallyjo.com/api/v1/email/messages/import" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"subject\": \"Weekly Newsletter — Imported\",
\"html\": \"<html><body>…<\\/body><\\/html>\",
\"rules\": [
{
\"id\": \"strip-tell-a-friend\",
\"find\": {
\"type\": \"xpath\",
\"pattern\": \"\\/\\/tr[.\\/\\/p[contains(., \'Tell a Friend\')]]\",
\"scope\": \"body\",
\"flags\": \"i\"
},
\"action\": {
\"type\": \"remove\",
\"with\": \"<img src=\\\"https:\\/\\/placehold.co\\/650x180\\/eeeeee\\/666666?text=Ad+Slot+{{counter}}\\\"\\/>\",
\"attr\": \"src\",
\"position\": \"before\"
},
\"stage\": \"dom\",
\"enabled\": true,
\"required\": false,
\"max\": 1,
\"notes\": \"Drops the address-book \\/ date row.\"
}
],
\"data_mappings\": [
{
\"name\": \"Featured sites\",
\"default\": false,
\"bindings\": [
{
\"category\": \"featured\",
\"source\": \"content_source\",
\"content_source_id\": 3,
\"count\": 3,
\"sort\": \"newest\",
\"require_image\": true,
\"literal\": [],
\"path\": \"item_1\"
}
]
}
]
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/email/messages/import"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"subject": "Weekly Newsletter — Imported",
"html": "<html><body>…<\/body><\/html>",
"rules": [
{
"id": "strip-tell-a-friend",
"find": {
"type": "xpath",
"pattern": "\/\/tr[.\/\/p[contains(., 'Tell a Friend')]]",
"scope": "body",
"flags": "i"
},
"action": {
"type": "remove",
"with": "<img src=\"https:\/\/placehold.co\/650x180\/eeeeee\/666666?text=Ad+Slot+{{counter}}\"\/>",
"attr": "src",
"position": "before"
},
"stage": "dom",
"enabled": true,
"required": false,
"max": 1,
"notes": "Drops the address-book \/ date row."
}
],
"data_mappings": [
{
"name": "Featured sites",
"default": false,
"bindings": [
{
"category": "featured",
"source": "content_source",
"content_source_id": 3,
"count": 3,
"sort": "newest",
"require_image": true,
"literal": [],
"path": "item_1"
}
]
}
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/email/messages/import'
payload = {
"subject": "Weekly Newsletter — Imported",
"html": "<html><body>…<\/body><\/html>",
"rules": [
{
"id": "strip-tell-a-friend",
"find": {
"type": "xpath",
"pattern": "\/\/tr[.\/\/p[contains(., 'Tell a Friend')]]",
"scope": "body",
"flags": "i"
},
"action": {
"type": "remove",
"with": "<img src=\"https:\/\/placehold.co\/650x180\/eeeeee\/666666?text=Ad+Slot+{{counter}}\"\/>",
"attr": "src",
"position": "before"
},
"stage": "dom",
"enabled": true,
"required": false,
"max": 1,
"notes": "Drops the address-book \/ date row."
}
],
"data_mappings": [
{
"name": "Featured sites",
"default": false,
"bindings": [
{
"category": "featured",
"source": "content_source",
"content_source_id": 3,
"count": 3,
"sort": "newest",
"require_image": true,
"literal": [],
"path": "item_1"
}
]
}
]
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->post(
'https://www.sallyjo.com/api/v1/email/messages/import',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'subject' => 'Weekly Newsletter — Imported',
'html' => '<html><body>…</body></html>',
'rules' => [
[
'id' => 'strip-tell-a-friend',
'find' => [
'type' => 'xpath',
'pattern' => '//tr[.//p[contains(., \'Tell a Friend\')]]',
'scope' => 'body',
'flags' => 'i',
],
'action' => [
'type' => 'remove',
'with' => '<img src="https://placehold.co/650x180/eeeeee/666666?text=Ad+Slot+{{counter}}"/>',
'attr' => 'src',
'position' => 'before',
],
'stage' => 'dom',
'enabled' => true,
'required' => false,
'max' => 1,
'notes' => 'Drops the address-book / date row.',
],
],
'data_mappings' => [
[
'name' => 'Featured sites',
'default' => false,
'bindings' => [
[
'category' => 'featured',
'source' => 'content_source',
'content_source_id' => 3,
'count' => 3,
'sort' => 'newest',
'require_image' => true,
'literal' => [],
'path' => 'item_1',
],
],
],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("Weekly Newsletter — Imported"), "subject");
data.Add(new StringContent("<html><body>…</body></html>"), "html");
data.Add(new StringContent("strip-tell-a-friend"), "rules[][id]");
data.Add(new StringContent("xpath"), "rules[][find][type]");
data.Add(new StringContent("//tr[.//p[contains(., 'Tell a Friend')]]"), "rules[][find][pattern]");
data.Add(new StringContent("body"), "rules[][find][scope]");
data.Add(new StringContent("i"), "rules[][find][flags]");
data.Add(new StringContent("remove"), "rules[][action][type]");
data.Add(new StringContent("<img src="https://placehold.co/650x180/eeeeee/666666?text=Ad+Slot+{{counter}}"/>"), "rules[][action][with]");
data.Add(new StringContent("src"), "rules[][action][attr]");
data.Add(new StringContent("before"), "rules[][action][position]");
data.Add(new StringContent("dom"), "rules[][stage]");
data.Add(new StringContent("1"), "rules[][enabled]");
data.Add(new StringContent(""), "rules[][required]");
data.Add(new StringContent("1"), "rules[][max]");
data.Add(new StringContent("Drops the address-book / date row."), "rules[][notes]");
data.Add(new StringContent("Featured sites"), "data_mappings[][name]");
data.Add(new StringContent(""), "data_mappings[][default]");
data.Add(new StringContent("featured"), "data_mappings[][bindings][][category]");
data.Add(new StringContent("content_source"), "data_mappings[][bindings][][source]");
data.Add(new StringContent("3"), "data_mappings[][bindings][][content_source_id]");
data.Add(new StringContent("3"), "data_mappings[][bindings][][count]");
data.Add(new StringContent("newest"), "data_mappings[][bindings][][sort]");
data.Add(new StringContent("1"), "data_mappings[][bindings][][require_image]");
data.Add(new StringContent("item_1"), "data_mappings[][bindings][][path]");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/email/messages/import"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (201, Success):
{
"id": 174,
"subject": "Weekly Newsletter — Imported",
"edit_url": "https://app.sallyjo.com/email/message/174/edit/content",
"mjml_url": "https://app.sallyjo.com/email/message/174/mjml",
"rule_results": [
{
"id": "strip-tell-a-friend",
"matched": 1,
"applied": 1,
"error": null
},
{
"id": "replace-liveintent-ads",
"matched": 4,
"applied": 4,
"error": null
}
],
"warnings": [],
"data_mappings": [],
"default_mapping_id": null
}
Example response (422, A required rule matched zero elements — nothing was saved):
{
"message": "One or more rules failed; nothing was saved.",
"rule_results": [
{
"id": "insert-header-universal-element-marker",
"matched": 0,
"applied": 0,
"error": "required rule matched 0 nodes"
}
],
"warnings": []
}
Example response (422, Validation error):
{
"message": "The subject field is required.",
"errors": {
"subject": [
"The subject field is required."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Dry-run an HTML import
requires authentication
Runs the same conversion (and any rules[] / data_mappings[]) as
POST /email/messages/import but persists nothing. Returns the
mjml_json the editor would receive along with per-rule results and
warnings so you can iterate on source HTML and rule sets without
littering the team with throwaway creatives.
The request body is identical to POST /email/messages/import; see
that endpoint's docs for the full field catalog. This endpoint never
returns 422 for "required rule matched 0 nodes" — instead the failure
shows up in rule_results[].error so you can inspect exactly which
rules missed and iterate on them.
Example request:
curl --request POST \
"https://www.sallyjo.com/api/v1/email/messages/import/dry-run" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"subject\": \"Weekly Newsletter — Imported\",
\"html\": \"<html><body>…<\\/body><\\/html>\",
\"rules\": [
{
\"id\": \"-\",
\"find\": {
\"type\": \"text\",
\"pattern\": \"molestiae\",
\"scope\": \"document\",
\"flags\": \"consectetur\"
},
\"action\": {
\"type\": \"replace\",
\"with\": \"ipsa\",
\"attr\": \"provident\",
\"position\": \"after\"
},
\"stage\": \"dom\"
}
],
\"data_mappings\": [
{
\"name\": \"vvwqiyszaxlgca\",
\"default\": false,
\"bindings\": [
{
\"category\": \"etfrqv\",
\"source\": \"literal\",
\"content_source_id\": 35,
\"count\": 22,
\"sort\": \"random\",
\"require_image\": false,
\"path\": \"frahrvlatiuupittvokddoxfa\"
}
]
}
]
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/email/messages/import/dry-run"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"subject": "Weekly Newsletter — Imported",
"html": "<html><body>…<\/body><\/html>",
"rules": [
{
"id": "-",
"find": {
"type": "text",
"pattern": "molestiae",
"scope": "document",
"flags": "consectetur"
},
"action": {
"type": "replace",
"with": "ipsa",
"attr": "provident",
"position": "after"
},
"stage": "dom"
}
],
"data_mappings": [
{
"name": "vvwqiyszaxlgca",
"default": false,
"bindings": [
{
"category": "etfrqv",
"source": "literal",
"content_source_id": 35,
"count": 22,
"sort": "random",
"require_image": false,
"path": "frahrvlatiuupittvokddoxfa"
}
]
}
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/email/messages/import/dry-run'
payload = {
"subject": "Weekly Newsletter — Imported",
"html": "<html><body>…<\/body><\/html>",
"rules": [
{
"id": "-",
"find": {
"type": "text",
"pattern": "molestiae",
"scope": "document",
"flags": "consectetur"
},
"action": {
"type": "replace",
"with": "ipsa",
"attr": "provident",
"position": "after"
},
"stage": "dom"
}
],
"data_mappings": [
{
"name": "vvwqiyszaxlgca",
"default": false,
"bindings": [
{
"category": "etfrqv",
"source": "literal",
"content_source_id": 35,
"count": 22,
"sort": "random",
"require_image": false,
"path": "frahrvlatiuupittvokddoxfa"
}
]
}
]
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->post(
'https://www.sallyjo.com/api/v1/email/messages/import/dry-run',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'subject' => 'Weekly Newsletter — Imported',
'html' => '<html><body>…</body></html>',
'rules' => [
[
'id' => '-',
'find' => [
'type' => 'text',
'pattern' => 'molestiae',
'scope' => 'document',
'flags' => 'consectetur',
],
'action' => [
'type' => 'replace',
'with' => 'ipsa',
'attr' => 'provident',
'position' => 'after',
],
'stage' => 'dom',
],
],
'data_mappings' => [
[
'name' => 'vvwqiyszaxlgca',
'default' => false,
'bindings' => [
[
'category' => 'etfrqv',
'source' => 'literal',
'content_source_id' => 35,
'count' => 22,
'sort' => 'random',
'require_image' => false,
'path' => 'frahrvlatiuupittvokddoxfa',
],
],
],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("Weekly Newsletter — Imported"), "subject");
data.Add(new StringContent("<html><body>…</body></html>"), "html");
data.Add(new StringContent("-"), "rules[][id]");
data.Add(new StringContent("text"), "rules[][find][type]");
data.Add(new StringContent("molestiae"), "rules[][find][pattern]");
data.Add(new StringContent("document"), "rules[][find][scope]");
data.Add(new StringContent("consectetur"), "rules[][find][flags]");
data.Add(new StringContent("replace"), "rules[][action][type]");
data.Add(new StringContent("ipsa"), "rules[][action][with]");
data.Add(new StringContent("provident"), "rules[][action][attr]");
data.Add(new StringContent("after"), "rules[][action][position]");
data.Add(new StringContent("dom"), "rules[][stage]");
data.Add(new StringContent("vvwqiyszaxlgca"), "data_mappings[][name]");
data.Add(new StringContent(""), "data_mappings[][default]");
data.Add(new StringContent("etfrqv"), "data_mappings[][bindings][][category]");
data.Add(new StringContent("literal"), "data_mappings[][bindings][][source]");
data.Add(new StringContent("35"), "data_mappings[][bindings][][content_source_id]");
data.Add(new StringContent("22"), "data_mappings[][bindings][][count]");
data.Add(new StringContent("random"), "data_mappings[][bindings][][sort]");
data.Add(new StringContent(""), "data_mappings[][bindings][][require_image]");
data.Add(new StringContent("frahrvlatiuupittvokddoxfa"), "data_mappings[][bindings][][path]");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/email/messages/import/dry-run"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200, Success):
{
"mjml_json": {
"content": {
"type": "page",
"data": {
"preheader": "AllFreeKnitting for {{ date.current }}"
},
"children": [
{
"type": "standard-section",
"uid": "9",
"title": "KP Header",
"children": [],
"attributes": {}
}
]
}
},
"rule_results": [
{
"id": "insert-header-universal-element-marker",
"matched": 1,
"applied": 1,
"error": null
},
{
"id": "strip-header-topnav-row",
"matched": 1,
"applied": 1,
"error": null
},
{
"id": "override-preheader",
"matched": 1,
"applied": 1,
"error": null
}
],
"warnings": []
}
Example response (422, Validation error):
{
"message": "The html field is required.",
"errors": {
"html": [
"The html field is required."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Link management
APIs for managing emails
Create a short link
requires authentication
Creates a team-scoped short URL that permanently 302-redirects to
destination_url. The short key is deterministic per team +
destination pair, so calling this endpoint twice with the same
destination_url returns the same short link (idempotent).
The returned record includes a default_short_url string that is the
fully qualified short URL to hand to end users.
Example request:
curl --request POST \
"https://www.sallyjo.com/api/v1/links/create" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"destination_url\": \"https:\\/\\/www.example.com\\/landing?utm_source=email\"
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/links/create"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"destination_url": "https:\/\/www.example.com\/landing?utm_source=email"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/links/create'
payload = {
"destination_url": "https:\/\/www.example.com\/landing?utm_source=email"
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->post(
'https://www.sallyjo.com/api/v1/links/create',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'destination_url' => 'https://www.example.com/landing?utm_source=email',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("https://www.example.com/landing?utm_source=email"), "destination_url");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/links/create"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200, Success):
{
"id": 42,
"team_id": 1,
"destination_url": "https://www.example.com/landing?utm_source=email",
"url_key": "aB3xY7q",
"redirect_status_code": 302,
"single_use": false,
"track_visits": true,
"default_short_url": "https://sjo.link/aB3xY7q",
"created_at": "2026-07-15T16:20:00.000000Z",
"updated_at": "2026-07-15T16:20:00.000000Z"
}
Example response (422, Missing or malformed destination):
{
"message": "The destination url field is required.",
"errors": {
"destination_url": [
"The destination url field is required."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Push Notifications
APIs for push notification subscriptions
Subscribe to push notifications (single list)
This endpoint allows a browser to subscribe to push notifications for a specific list. The endpoint is public and does not require authentication.
Example request:
curl --request POST \
"https://www.sallyjo.com/api/v1/push/subscribe" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"list_slug\": \"my-notifications\",
\"endpoint\": \"https:\\/\\/fcm.googleapis.com\\/fcm\\/send\\/...\",
\"p256dh\": \"BNVAPKu...\",
\"auth\": \"abc123...\",
\"expiration_time\": 18,
\"domain\": \"example.com\",
\"browser\": \"Chrome\",
\"platform\": \"Win32\",
\"categories\": [
\"gpohtxsrsjcqewumhbtegeg\"
]
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/push/subscribe"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"list_slug": "my-notifications",
"endpoint": "https:\/\/fcm.googleapis.com\/fcm\/send\/...",
"p256dh": "BNVAPKu...",
"auth": "abc123...",
"expiration_time": 18,
"domain": "example.com",
"browser": "Chrome",
"platform": "Win32",
"categories": [
"gpohtxsrsjcqewumhbtegeg"
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/push/subscribe'
payload = {
"list_slug": "my-notifications",
"endpoint": "https:\/\/fcm.googleapis.com\/fcm\/send\/...",
"p256dh": "BNVAPKu...",
"auth": "abc123...",
"expiration_time": 18,
"domain": "example.com",
"browser": "Chrome",
"platform": "Win32",
"categories": [
"gpohtxsrsjcqewumhbtegeg"
]
}
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->post(
'https://www.sallyjo.com/api/v1/push/subscribe',
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'list_slug' => 'my-notifications',
'endpoint' => 'https://fcm.googleapis.com/fcm/send/...',
'p256dh' => 'BNVAPKu...',
'auth' => 'abc123...',
'expiration_time' => 18,
'domain' => 'example.com',
'browser' => 'Chrome',
'platform' => 'Win32',
'categories' => [
'gpohtxsrsjcqewumhbtegeg',
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("my-notifications"), "list_slug");
data.Add(new StringContent("https://fcm.googleapis.com/fcm/send/..."), "endpoint");
data.Add(new StringContent("BNVAPKu..."), "p256dh");
data.Add(new StringContent("abc123..."), "auth");
data.Add(new StringContent("18"), "expiration_time");
data.Add(new StringContent("example.com"), "domain");
data.Add(new StringContent("Chrome"), "browser");
data.Add(new StringContent("Win32"), "platform");
data.Add(new StringContent("gpohtxsrsjcqewumhbtegeg"), "categories[]");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/push/subscribe"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (201):
{
"success": true,
"subscription_id": "123",
"message": "Successfully subscribed to push notifications"
}
Example response (422):
{
"message": "The given data was invalid.",
"errors": {
"list_slug": [
"The specified push notification list does not exist."
]
}
}
Example response (429):
{
"message": "Too many subscription attempts. Please try again later."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Batch subscribe to push notifications (multiple lists)
Subscribe a browser to multiple push notification lists at once. Each list requires its own push subscription (different VAPID key = different endpoint). Used by prompts that target multiple lists.
Example request:
curl --request POST \
"https://www.sallyjo.com/api/v1/push/batch-subscribe" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"prompt_id\": \"550e8400-e29b-41d4-a716-446655440000\",
\"subscriptions\": [
\"animi\"
],
\"domain\": \"example.com\",
\"browser\": \"Chrome\",
\"platform\": \"Win32\"
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/push/batch-subscribe"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"prompt_id": "550e8400-e29b-41d4-a716-446655440000",
"subscriptions": [
"animi"
],
"domain": "example.com",
"browser": "Chrome",
"platform": "Win32"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/push/batch-subscribe'
payload = {
"prompt_id": "550e8400-e29b-41d4-a716-446655440000",
"subscriptions": [
"animi"
],
"domain": "example.com",
"browser": "Chrome",
"platform": "Win32"
}
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->post(
'https://www.sallyjo.com/api/v1/push/batch-subscribe',
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'prompt_id' => '550e8400-e29b-41d4-a716-446655440000',
'subscriptions' => [
'animi',
],
'domain' => 'example.com',
'browser' => 'Chrome',
'platform' => 'Win32',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("550e8400-e29b-41d4-a716-446655440000"), "prompt_id");
data.Add(new StringContent("animi"), "subscriptions[]");
data.Add(new StringContent("example.com"), "domain");
data.Add(new StringContent("Chrome"), "browser");
data.Add(new StringContent("Win32"), "platform");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/push/batch-subscribe"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (201):
{
"success": true,
"results": [
{
"list_slug": "news",
"subscription_id": "1",
"status": "created"
},
{
"list_slug": "deals",
"subscription_id": "2",
"status": "created"
}
]
}
Example response (422):
{
"message": "The given data was invalid."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get prompt configuration
Returns the prompt configuration, theme, tracking URLs, and list VAPID keys. Used by the embed script on external sites to render the prompt UI.
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/push/prompt/hic" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/push/prompt/hic"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/push/prompt/hic'
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/push/prompt/hic',
[
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/push/prompt/hic"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
{
"uuid": "550e8400...",
"type": "slide",
"position": "bottom_center",
"lists": [
{
"listId": 1,
"slug": "news",
"publicKey": "BN..."
}
],
"tracking": {
"impression": "https://..."
}
}
Example response (404):
{
"message": "Prompt not found or disabled."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
SMS List management
APIs for managing SMS lists and subscriptions
Get SMS list subscribers
requires authentication
Returns a paginated list of subscribers for the specified SMS list.
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/sms/lists/12/subscribers?phone_number=%2B1555&status=active&per_page=15" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/sms/lists/12/subscribers"
);
const params = {
"phone_number": "+1555",
"status": "active",
"per_page": "15",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/sms/lists/12/subscribers'
params = {
'phone_number': '+1555',
'status': 'active',
'per_page': '15',
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, params=params)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/sms/lists/12/subscribers',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'phone_number' => '+1555',
'status' => 'active',
'per_page' => '15',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/sms/lists/12/subscribers?phone_number=%2B1555&status=active&per_page=15"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
{
"data": [],
"current_page": 1,
"total": 0
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create SMS list subscription
requires authentication
Add a phone number to an SMS list. Automatically cleans and validates the phone number.
Example request:
curl --request POST \
"https://www.sallyjo.com/api/v1/sms/lists/13/subscribers" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"phone_number\": \"+15551234567\",
\"contact_id\": 42,
\"assume_country_code\": 1
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/sms/lists/13/subscribers"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"phone_number": "+15551234567",
"contact_id": 42,
"assume_country_code": 1
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/sms/lists/13/subscribers'
payload = {
"phone_number": "+15551234567",
"contact_id": 42,
"assume_country_code": 1
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->post(
'https://www.sallyjo.com/api/v1/sms/lists/13/subscribers',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'phone_number' => '+15551234567',
'contact_id' => 42,
'assume_country_code' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("+15551234567"), "phone_number");
data.Add(new StringContent("42"), "contact_id");
data.Add(new StringContent("1"), "assume_country_code");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/sms/lists/13/subscribers"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (201):
{
"id": 1,
"phone_number": "+15551234567",
"status": "active"
}
Example response (422):
{
"message": "Validation failed",
"errors": {
"phone_number": [
"Invalid phone number"
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
SMS management
APIs for managing text messages
Send an SMS/MMS message
requires authentication
Send a text message or multimedia message via Twilio. Include media_urls to send as MMS.
Example request:
curl --request POST \
"https://www.sallyjo.com/api/v1/sms/send" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"to\": \"+15551234567\",
\"from\": \"+15559876543\",
\"message\": \"Hello from our team!\",
\"media_urls\": [
\"https:\\/\\/example.com\\/image.jpg\"
]
}"
const url = new URL(
"https://www.sallyjo.com/api/v1/sms/send"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"to": "+15551234567",
"from": "+15559876543",
"message": "Hello from our team!",
"media_urls": [
"https:\/\/example.com\/image.jpg"
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/sms/send'
payload = {
"to": "+15551234567",
"from": "+15559876543",
"message": "Hello from our team!",
"media_urls": [
"https:\/\/example.com\/image.jpg"
]
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->post(
'https://www.sallyjo.com/api/v1/sms/send',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'to' => '+15551234567',
'from' => '+15559876543',
'message' => 'Hello from our team!',
'media_urls' => [
'https://example.com/image.jpg',
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var data = new MultipartFormDataContent();
data.Add(new StringContent("+15551234567"), "to");
data.Add(new StringContent("+15559876543"), "from");
data.Add(new StringContent("Hello from our team!"), "message");
data.Add(new StringContent("https://example.com/image.jpg"), "media_urls[]");
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/sms/send"),
Content = data
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
{
"success": true,
"sid": "SM...",
"sent_id": "..."
}
Example response (422):
{
"error": "recipient_unsubscribed",
"message": "Recipient has opted out and cannot receive messages."
}
Example response (422):
{
"error": "not_mobile_number",
"message": "The destination is not a valid mobile number."
}
Example response (422):
{
"message": "The to field is required.",
"errors": {
"to": [
"The to field is required."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Team management
APIs for managing teams
Get the authenticated team
requires authentication
Returns the team that owns the API token used to make this request.
Note: API auth resolves to a Team model, not a User. If your
client expects a user object, this is where clients usually go wrong —
every $request->user() call in the api/v1/* group returns the team
whose token authenticated the request.
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/team" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/team"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/team'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/team',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/team"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200, Success):
{
"id": 1,
"user_id": 42,
"name": "Acme, Inc.",
"personal_team": false,
"current_plan": "growth",
"yearly_billing": false,
"manual_billing": false,
"billing_period_start": "2026-06-01T00:00:00.000000Z",
"billing_period_end": "2026-07-01T00:00:00.000000Z",
"monthly_allowances": {
"emails": 50000,
"sms": 1000
},
"created_at": "2024-01-15T10:30:00.000000Z",
"updated_at": "2026-06-01T00:00:00.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Verified Identities
APIs for retrieving verified sender identities.
Before sending emails or SMS messages through the API, you need verified sender identities. These endpoints allow you to retrieve your team's verified email addresses and phone numbers.
Email Verification
Email addresses are verified through AWS SES. When you add a new email address, a verification email is sent to that address. Once verified, you can use it as the "from" address when sending emails.
Phone Verification
Phone numbers are verified through Twilio. These are typically purchased or ported numbers that have been registered with your Twilio account and linked to your team.
Using Verified Identities
- Use
GET /api/v1/verified-identities/emailsto list available email addresses for sending - Use
GET /api/v1/verified-identities/phonesto list available phone numbers for SMS - Pass the email/phone value to the respective send endpoints
List all verified identities
requires authentication
Get all verified identities (emails, phones, and domains) for this team. Useful for getting a complete overview of available sender identities.
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/verified-identities?type=email" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/verified-identities"
);
const params = {
"type": "email",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/verified-identities'
params = {
'type': 'email',
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, params=params)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/verified-identities',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'type' => 'email',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/verified-identities?type=email"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
{
"emails": [
{
"id": 1,
"email": "hello@example.com",
"verified_at": "2024-01-15T10:30:00Z"
}
],
"phones": [
{
"id": 1,
"phone_number": "+14155552671",
"type": "twilio",
"verified_at": "2024-01-15T10:30:00Z"
}
],
"domains": [
{
"id": 1,
"domain": "example.com",
"verified_at": "2024-01-10T09:00:00Z"
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List verified email addresses
requires authentication
Get all verified email addresses that can be used as sender addresses for this team. Only returns emails that have completed verification.
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/verified-identities/emails" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/verified-identities/emails"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/verified-identities/emails'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/verified-identities/emails',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/verified-identities/emails"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
[
{
"id": 1,
"email": "hello@example.com",
"verified_at": "2024-01-15T10:30:00Z"
},
{
"id": 2,
"email": "support@example.com",
"verified_at": "2024-01-20T14:45:00Z"
}
]
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List verified phone numbers
requires authentication
Get all verified phone numbers that can be used as sender numbers for SMS messages. Phone numbers are in E.164 format (e.g., +14155552671).
Example request:
curl --request GET \
--get "https://www.sallyjo.com/api/v1/verified-identities/phones" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://www.sallyjo.com/api/v1/verified-identities/phones"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'https://www.sallyjo.com/api/v1/verified-identities/phones'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()$client = new \GuzzleHttp\Client();
$response = $client->get(
'https://www.sallyjo.com/api/v1/verified-identities/phones',
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_KEY}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
using System.Net.Http.Headers;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://www.sallyjo.com/api/v1/verified-identities/phones"),
};
using (var response = await client.SendAsync(request))
{
//response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}Example response (200):
[
{
"id": 1,
"phone_number": "+14155552671",
"type": "twilio",
"verified_at": "2024-01-15T10:30:00Z"
},
{
"id": 2,
"phone_number": "+14155552672",
"type": "twilio",
"verified_at": "2024-01-20T14:45:00Z"
}
]
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.