MENU navbar-image

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:

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:

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
    }
]
 

Request      

GET api/v1/contact-fields

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

Query Parameters

contact_type   string  optional  

Filter by contact type. Must be person or organization. Example: person

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"
    }
]
 

Request      

GET api/v1/contact-fields/types

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

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."
}
 

Request      

POST api/v1/contact-fields

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

Body Parameters

name   string   

The display name for the field. Example: Membership Level

tag   string   

The merge tag identifier (lowercase, underscores, unique per team). Example: membership_level

type   string   

The field type. Must be one of: text, email, phone, number, date, datetime, boolean, url, pick_list, related. Example: pick_list

contact_type   string   

The contact type this field belongs to. Must be person or organization. Example: person

max_length   integer  optional  

The maximum length for text fields. Default: 255. Example: 100

options   string[]  optional  

An array of options for pick_list fields.

allow_multiple   boolean  optional  

Whether multiple values can be selected (for pick_list). Default: false. Example: false

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]"
}
 

Request      

GET api/v1/contact-fields/{id}

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

id   integer   

The ID of the contact field. Example: 8

contact_field   integer   

The contact field ID. Example: 1

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."
}
 

Request      

PUT api/v1/contact-fields/{id}

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

id   integer   

The ID of the contact field. Example: 16

contact_field   integer   

The contact field ID. Example: 1

Body Parameters

name   string  optional  

The display name for the field. Example: Full Name

tag   string  optional  

The merge tag identifier (lowercase, underscores, unique per team). Example: full_name

type   string  optional  

The field type. Must be one of: text, email, phone, number, date, datetime, boolean, url, pick_list, related. Example: text

max_length   integer  optional  

The maximum length for text fields. Example: 150

options   string[]  optional  

An array of options for pick_list fields.

allow_multiple   boolean  optional  

Whether multiple values can be selected (for pick_list). Example: true

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."
}
 

Request      

DELETE api/v1/contact-fields/{id}

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

id   integer   

The ID of the contact field. Example: 20

contact_field   integer   

The contact field ID. Example: 1

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

Example Workflow

  1. First, fetch your team's contact fields using GET /api/v1/contact-fields
  2. Use the field tag values as keys when creating/updating contacts
  3. The field type tells 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."
        ]
    }
}
 

Request      

GET api/v1/contacts

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

Query Parameters

page   integer  optional  

Page number to return. Must be at least 1. Example: 1

per_page   integer  optional  

Number of records per page (default: 15, max: 100). Example: 15

Body Parameters

page   integer  optional  

Must be at least 1. Example: 3

per_page   integer  optional  

Must be at least 1. Must not be greater than 100. Example: 19

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

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."
        ]
    }
}
 

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]"
}
 

Request      

POST api/v1/contacts/create

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

Body Parameters

type   string   

The contact type. Must be either "person" or "organization". Example: person

first_name   string  optional  

The contact's first name. Example: John

last_name   string  optional  

The contact's last name. Example: Doe

email   string  optional  

The contact's email address. Must be a valid email format. Example: john@example.com

phone   string  optional  

The contact's phone number. Will be normalized to E.164 format. Example: +14155552671

birthday   string  optional  

The contact's birthday (date format). Example: 1990-05-15

address   string  optional  

The contact's street address. Example: 123 Main Street

city   string  optional  

The contact's city. Example: San Francisco

state   string  optional  

The contact's state. Example: CA

zipcode   string  optional  

The contact's zip/postal code. Example: 94102

tags   string[]  optional  

An array of tag slugs to assign to the contact. All tags must exist in your team's tag collection.

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]."
}
 

Request      

GET api/v1/contacts/{contact_id}

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

contact_id   integer   

The ID of the contact. Example: 11

contact   integer   

The contact ID. Example: 1

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"
}
 

Request      

PUT api/v1/contacts/{id}/update

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

id   integer   

The ID of the contact. Example: 19

contact   integer   

The contact ID. Example: 1

Body Parameters

first_name   string  optional  

The contact's first name. Example: Jane

last_name   string  optional  

The contact's last name. Example: Smith

email   string  optional  

The contact's email address. Must be a valid email format. Example: jane@example.com

phone   string  optional  

The contact's phone number. Will be normalized to E.164 format. Example: +14155552672

birthday   string  optional  

The contact's birthday (date format). Example: 1985-08-22

address   string  optional  

The contact's street address. Example: 456 Oak Avenue

city   string  optional  

The contact's city. Example: Los Angeles

state   string  optional  

The contact's state. Example: CA

zipcode   string  optional  

The contact's zip/postal code. Example: 90210

tags   string[]  optional  

An array of tag slugs to assign to the contact. Pass null or empty array to remove all tags. All tags must exist in your team's tag collection.

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]."
}
 

Request      

DELETE api/v1/contacts/{contact_id}

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

contact_id   integer   

The ID of the contact. Example: 9

contact   integer   

The contact ID. Example: 1

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"
        }
    }
]
 

Request      

GET api/v1/lists/{list_id}/subscribers

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

list_id   integer   

The ID of the list. Example: 5

list   integer   

The email list ID. Example: 1

Query Parameters

limit   integer  optional  

Limit number of records in the search. Example: 20

order_by   string  optional  

Field tag to order the results by. Example: last_name

page   integer  optional  

Page to return results for. Example: 5

email   string  optional  

Filter by exact email address. Example: subscriber@example.com

status   string  optional  

Filter by subscription status (active, unsubscribed, bounced, complained). Example: active

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"
}
 

Request      

POST api/v1/lists/{list_id}/subscribers

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

list_id   integer   

The ID of the list. Example: 8

list   integer   

The email list ID. Example: 1

Query Parameters

email   string   

Must be a valid email address. Example: maximilian48@example.com

contact_id   integer  optional  

Example: 17

limit   integer  optional  

Limit number of records in the search. Example: 8

order_by   string  optional  

Field tag to order the results by. Example: last_name

page   integer  optional  

Page to return results for. Example: 17

Body Parameters

email   string   

The email address to subscribe. Example: subscriber@example.com

contact_id   integer  optional  

Optional contact ID to link the subscription to. Example: 123

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"
}
 

Request      

PUT api/v1/subscriptions/{id}/update

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

id   integer   

The ID of the subscription. Example: 11

subscription   integer   

The subscription ID. Example: 1

Body Parameters

contact_id   integer  optional  

The contact ID to associate with this subscription. Example: 123

status   string  optional  

The subscription status (active, unsubscribed, bounced, complained). Example: active

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

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."
        ]
    }
}
 

Request      

POST api/v1/email/send

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: multipart/form-data

Accept      

Example: application/json

Body Parameters

to   string   

Recipient email address. Must be a valid, non-banned address. Example: subscriber@example.com

from   string   

Sender email address. Must be a verified identity on the team (see GET /api/v1/verified-identities/emails). Example: hello@acme.com

subject   string  optional  

Subject line. Required when sending raw content (message_id not supplied). Example: Your weekly digest

html   string  optional  

HTML body. Optional if plaintext or message_id is supplied. Example: <h1>Hi!</h1><p>Welcome.</p>

plaintext   string  optional  

Plain-text body. Falls back to a stripped version of html when omitted. Example: et

message_id   integer  optional  

ID of an existing Email\Message on the team. If supplied, subject / html / plaintext are ignored. Example: 174

campaign   string  optional  
merge_data   string  optional  

JSON-encoded object of additional merge-tag values to interpolate into the message. Example: {"first_name":"Alex","promo_code":"WELCOME10"}

cc   string[]  optional  

CC recipient email addresses (each must be a valid, non-banned address).

bcc   string[]  optional  

BCC recipient email addresses (each must be a valid, non-banned address).

attachments   file[]  optional  

File uploads to attach (multipart/form-data only). Stored under messages/{message_id}/ on the team's S3 bucket.

campaign_id   integer  optional  

ID of an Email\Campaign to attribute the send to. Enables list-scoped merge tags and reporting. Example: 12

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."
        ]
    }
}
 

Request      

POST api/v1/email/messages/import

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

Body Parameters

subject   string   

Subject line for the new creative. Max 255 characters. Example: Weekly Newsletter — Imported

html   string   

Raw HTML source of the creative. Max 5 MB. Example: <html><body>…</body></html>

rules   object[]  optional  

Import-rule payloads to run before conversion. Order-sensitive: each dom-stage rule sees the mutations from earlier rules. See method docblock for shape and links to the reference rulesets.

id   string   

Kebab-case rule identifier. Must match ^[a-z0-9-]+$. Example: strip-tell-a-friend

find   object   

Matcher spec.

type   string   

One of css, xpath, regex, text. Example: xpath

pattern   string   

The selector / expression / substring to match. Example: //tr[.//p[contains(., 'Tell a Friend')]]

scope   string  optional  

For css / xpath, restrict evaluation to body or the whole document. Defaults to body. Example: body

flags   string  optional  

For regex, extra flags (e.g. i, s). Example: i

action   object   

Mutation spec.

type   string   

One of remove, replace, set-attr, remove-attr, set-text, wrap, unwrap, insert-adjacent. Example: remove

with   string  optional   " data-component="body">

Action payload — the replacement HTML (replace), attribute value (set-attr), text (set-text), or markup to inject (insert-adjacent, wrap). Supports templating tokens {{counter}} (1-based match index), {{@attr}} (existing attribute value), and merge-tag chips {{ ns.key | default:"Fallback" }}. Example: <img src="https://placehold.co/650x180/eeeeee/666666?text=Ad+Slot+{{counter}}"/>

attr   string  optional  

Attribute name for set-attr / remove-attr. Example: src

position   string  optional  

For insert-adjacent: before or after (sibling insertion). prepend / append are NOT supported — target the first / last child and use before / after instead. Example: before

stage   string  optional  

Rule stage. Either html (regex-only, runs on raw HTML) or dom (runs after DOM parse; supports css / xpath / regex / text find types). Defaults to dom. Example: dom

enabled   boolean  optional  

Set to false to skip the rule without removing it from the payload. Defaults to true. Example: true

required   boolean  optional  

If true and the rule matches zero elements, the whole import fails with 422. Use for structural landmarks that must exist in every creative. Defaults to false. Example: false

max   integer  optional  

Cap the number of matches this rule may act on. Extra matches are ignored. Example: 1

notes   string  optional  

Free-form notes (not consumed by the engine — helpful for reviewers). Example: Drops the address-book / date row.

data_mappings   object[]  optional  

Content-source bindings for merge-tag chips inserted by rules. Only ONE mapping may set default: true (first one wins). See docs/email-import-data-mappings.md.

name   string   

Display name for the mapping. Example: Featured sites

default   boolean  optional  

Mark as the default mapping used when previewing / sending without an explicit selection. Example: false

bindings   object[]   

At least one binding.

category   string   

Merge-tag namespace (e.g. spotlight, featured). Must match ^[A-Za-z_][A-Za-z0-9_]*$. Example: featured

source   string   

Either content_source (resolve from a Sally Jo content source at send time) or literal (use a static object). Example: content_source

content_source_id   integer  optional  

Required when source=content_source. The content source to pull items from. Example: 3

count   integer  optional  

Number of items to resolve (1..50). Example: 3

sort   string  optional  

One of newest, oldest, random. Example: newest

require_image   boolean  optional  

When true, resolver filters out items that have no rows in content_item_images. Set this for any binding that feeds image slots. Example: true

literal   object  optional  

Required when source=literal. Static object exposed under the namespace.

path   string  optional  

Optional sub-namespace under category, e.g. item_1. Restricted to [A-Za-z0-9_.-]. Example: item_1

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."
        ]
    }
}
 

Request      

POST api/v1/email/messages/import/dry-run

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

Body Parameters

subject   string   

Subject line (used only for logging in the dry-run — no creative is created). Max 255 characters. Example: Weekly Newsletter — Imported

html   string   

Raw HTML source of the creative. Max 5 MB. Example: <html><body>…</body></html>

rules   object[]  optional  

Import-rule payloads. Same shape as POST /email/messages/import.

id   string   

Must match the regex /^[a-z0-9-]+$/. Example: -

find   object   
type   string   

Example: text

pattern   string   

Example: molestiae

scope   string  optional  

Example: document

flags   string  optional  

Example: consectetur

action   object   
type   string   

Example: replace

with   string  optional  

Example: ipsa

attr   string  optional  

Example: provident

position   string  optional  

Example: after

stage   string  optional  

Example: dom

enabled   string  optional  
required   string  optional  
max   string  optional  
data_mappings   object[]  optional  

Content-source bindings. Same shape as POST /email/messages/import.

name   string   

Must not be greater than 255 characters. Example: vvwqiyszaxlgca

default   boolean  optional  

Example: false

bindings   object[]   

Must have at least 1 items.

category   string   

Must match the regex /^[A-Za-z][A-Za-z0-9]*$/. Must not be greater than 64 characters. Example: etfrqv

source   string   

Example: literal

content_source_id   integer  optional  

This field is required when data_mappings..bindings..source is content_source. Must be at least 1. Example: 35

count   integer  optional  

Must be at least 1. Must not be greater than 50. Example: 22

sort   string  optional  

Example: random

require_image   boolean  optional  

Example: false

literal   object  optional  

This field is required when data_mappings..bindings..source is literal.

path   string  optional  

Must match the regex /^[A-Za-z0-9_.-]*$/. Must not be greater than 120 characters. Example: frahrvlatiuupittvokddoxfa

Link management

APIs for managing emails

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."
        ]
    }
}
 

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."
}
 

Request      

POST api/v1/push/subscribe

Headers

Content-Type      

Example: application/json

Accept      

Example: application/json

Body Parameters

list_slug   string   

The slug of the push notification list. Example: my-notifications

endpoint   string   

The push subscription endpoint URL. Example: https://fcm.googleapis.com/fcm/send/...

p256dh   string   

The p256dh encryption key. Example: BNVAPKu...

auth   string   

The auth secret. Example: abc123...

expiration_time   integer  optional  

Example: 18

domain   string  optional  

The domain where the subscription was created. Example: example.com

browser   string  optional  

The browser name. Example: Chrome

platform   string  optional  

The platform/OS. Example: Win32

categories   string[]  optional  

Must not be greater than 100 characters.

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."
}
 

Request      

POST api/v1/push/batch-subscribe

Headers

Content-Type      

Example: application/json

Accept      

Example: application/json

Body Parameters

prompt_id   string  optional  

The UUID of the prompt that initiated the subscription. Example: 550e8400-e29b-41d4-a716-446655440000

subscriptions   string[]   

Array of subscription objects.

list_slug   string   

The slug of the push notification list. Example: my-notifications

endpoint   string   

The push subscription endpoint URL. Example: https://fcm.googleapis.com/fcm/send/...

p256dh   string   

The p256dh encryption key. Example: BNVAPKu...

auth   string   

The auth secret. Example: abc123...

categories   string[]  optional  

Must not be greater than 100 characters.

domain   string  optional  

The domain where the subscriptions were created. Example: example.com

browser   string  optional  

The browser name. Example: Chrome

platform   string  optional  

The platform/OS. Example: Win32

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."
}
 

Request      

GET api/v1/push/prompt/{prompt_uuid_id}

Headers

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

prompt_uuid_id   string   

The ID of the prompt uuid. Example: hic

prompt   string   

The UUID of the prompt. Example: 550e8400-e29b-41d4-a716-446655440000

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
}
 

Request      

GET api/v1/sms/lists/{list_id}/subscribers

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

list_id   integer   

The ID of the list. Example: 12

list   integer   

The SMS list ID. Example: 1

Query Parameters

phone_number   string  optional  

Filter subscribers by phone number (partial match). Example: +1555

status   string  optional  

Filter by subscription status. Example: active

per_page   integer  optional  

Number of results per page. Example: 15

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"
        ]
    }
}
 

Request      

POST api/v1/sms/lists/{list_id}/subscribers

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

URL Parameters

list_id   integer   

The ID of the list. Example: 13

list   integer   

The SMS list ID. Example: 1

Body Parameters

phone_number   string   

The phone number to subscribe. Example: +15551234567

contact_id   integer  optional  

The contact ID to associate with the subscription. Example: 42

assume_country_code   integer  optional  

Country code to assume if not provided. Defaults to 1 (US). Example: 1

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."
        ]
    }
}
 

Request      

POST api/v1/sms/send

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

Body Parameters

to   string   

The recipient phone number in E.164 format. Example: +15551234567

from   string   

The sender phone number. Must be a verified Twilio number for the team. Example: +15559876543

message   string   

The message body text. Example: Hello from our team!

media_urls   string[]  optional  

An array of public image URLs for MMS. Max 10 items. Supported formats: JPEG, PNG, GIF. Max 5MB per image.

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."
}
 

Request      

GET api/v1/team

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

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

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"
        }
    ]
}
 

Request      

GET api/v1/verified-identities

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

Query Parameters

type   string  optional  

Filter by identity type. Must be email, phone, or domain. Example: email

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"
    }
]
 

Request      

GET api/v1/verified-identities/emails

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json

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"
    }
]
 

Request      

GET api/v1/verified-identities/phones

Headers

Authorization      

Example: Bearer {YOUR_API_KEY}

Content-Type      

Example: application/json

Accept      

Example: application/json