Mailing lists & subscribers
Manage your audience: lists, the subscribers on them, segments, and custom fields. Paths are relative to https://app.blacklistguard.com/api/v1.
Lists
GET /mailing-lists
POST /mailing-lists
PUT /mailing-lists/{id}
DELETE /mailing-lists/{id}
GET /mailing-lists/stats?window=30
Create a list with:
| Field | Description |
|---|---|
name | List name. |
description | Optional description. |
from_name / from_email | Default sender for campaigns to this list. |
reply_to_email | Default reply-to. |
opt_in_type | Opt-in label for your reference: single, double, api, or import. |
stats returns per-list metrics over the window: new_subscribers, unsubscribed, net_growth, engagement_rate, bounce_rate, complaint_rate (rates are 0–1 fractions), health_score (0–100), a daily sparkline, and the last_campaign. Match each row to a list by id.
Subscribers (per list)
GET /mailing-lists/{id}/subscribers?page&limit&status&search
POST /mailing-lists/{id}/subscribers
PUT /mailing-lists/{id}/subscribers/{subscriber_id}
DELETE /mailing-lists/{id}/subscribers/{subscriber_id}
PUT /mailing-lists/{id}/subscribers/{subscriber_id}/status
DELETE /mailing-lists/{id}/subscribers/all
Add a subscriber with { email, first_name, last_name, source, custom_fields }. On update, email is not editable; you can change first_name, last_name, status, and custom_fields. status is one of active, unsubscribed, bounced, complained.
{subscriber_id} is the subscriber's UUID (not the membership row id), and each returned row's custom_fields is a JSON string, so parse it before use.Import & export
POST /mailing-lists/{id}/subscribers/import-async (multipart, field "csvFile")
POST /mailing-lists/{id}/subscribers/export-async
Both run as background jobs and return a job_id. Track progress via the jobs endpoints. The import CSV needs a header row with an Email column; First/Last/Status are optional and extra columns map to the list's defined custom fields.
Subscribers (workspace-wide)
GET /subscribers?page&limit&status&search&list_id
GET /subscribers/{id}
GET /subscribers/{id}/segments
Lists every subscriber across the workspace with a summary and pagination (here custom_fields is a JSON object). get(id) returns full detail: identity, the lists they're on, lifetime stats, an engagement score, recent activity, and their bounces.
Segments
GET /segments?page&limit&search&list_id
GET /mailing-lists/{id}/segments
POST /mailing-lists/{id}/segments
PUT /mailing-lists/{id}/segments/{segment_id}
DELETE /mailing-lists/{id}/segments/{segment_id}
POST /mailing-lists/{id}/segments/preview
GET /mailing-lists/{id}/segments/{segment_id}/subscribers
A segment is a saved filter on a list. Create one with { name, description, conditions, is_active }. conditions uses a recursive { logic, rules } shape, where logic is "and" or "or", and each rule is either a condition { field, operator, value } or a nested group with the same { logic, rules } shape:
{
"logic": "and",
"rules": [
{ "field": "country", "operator": "equals", "value": "US" },
{ "logic": "or", "rules": [
{ "field": "plan", "operator": "equals", "value": "growth" },
{ "field": "plan", "operator": "equals", "value": "scale" }
] }
]
}
preview takes the same conditions and returns { subscriber_count } without saving. Note is_active defaults to false unless you set it.
A segment can match subscribers of any status, so a rule like status is unsubscribed works for counting and exporting. Campaigns and autoresponders still only send to active members, whatever the segment says.
Rule operators
| Operator | Value | Use for |
|---|---|---|
equals, not_equals, contains, starts_with, ends_with | string | Text fields |
greater_than, less_than, greater_than_or_equal, less_than_or_equal | number | Numeric fields |
before, after | date / datetime string | Date fields |
in_last_days, not_in_last_days | integer (days) | Date fields |
in | array of strings | Text / select |
is_one_of, is_not_one_of | array of strings | e.g. email-domain match |
ends_with_any_of, does_not_end_with_any_of | array of strings | Suffix match |
is, is_not | scalar | Status / boolean fields |
is_in, is_not_in | a segment id | Segment membership |
is_active_in, is_not_active_in | a mailing-list id | Active membership in a list |
Portion rule
Besides conditions and groups, a rule can pick a share of the list. The share is random, but stable: each subscriber's place is fixed by their own ID and a key you choose, so people joining or leaving the list never move anyone else.
{ "type": "portion", "lower": 0, "upper": 20, "key": "ABCDE" }
| Field | Type | Description |
|---|---|---|
lower | number | Start of the range, a percentage from 0 to 100 with at most 2 decimals. Included. |
upper | number | End of the range, on the same scale. Excluded, unless it is 100. Must be above lower. |
key | string | 1 to 50 characters. Leave it out when you save a segment or a campaign's own conditions, and we generate one and return it in the saved conditions. |
Because upper is excluded, 0 to 50 and 50 to 100 never overlap, and 0 to 100 is the whole list. The same key picks the same subscribers wherever you use it, and rules that share a key with ranges that don't overlap always pick different subscribers. A different key picks an unrelated set.
You can use it on its own, inside a group, or next to other conditions. For example, 20% of the list who use Gmail:
{
"logic": "and",
"rules": [
{ "type": "portion", "lower": 0, "upper": 20, "key": "ABCDE" },
{ "field": "email", "operator": "contains", "value": "@gmail.com" }
]
}
preview doesn't save anything, so it doesn't generate a key. Include one, or the preview returns 400. A range or key that isn't valid also returns 400 BAD_REQUEST, with a message that says what's wrong, for example invalid segment rule: portion lower must be below upper.
This lets you split-test with separate campaigns: send version A to 0 to 10 and version B to 10 to 20, both with the same key, then send the better one to 20 to 100 with that key again. Nobody receives more than one. See Split tests across separate campaigns. To test versions inside a single campaign, use an A/B test instead.
Custom fields
GET /mailing-lists/{id}/custom-fields
POST /mailing-lists/{id}/custom-fields
PUT /mailing-lists/{id}/custom-fields/{field_id}
DELETE /mailing-lists/{id}/custom-fields/{field_id}
GET /custom-fields/global (workspace-wide fields)
Required: field_name, field_label, field_type. The field_name is normalized (lowercased, spaces become underscores) and locked once created. field_type is one of text, textarea, number, date, email, url, boolean, select, multiselect; for select and multiselect you must also supply field_options (a JSON array of choices). Optional: placeholder, default_value, help_text, validation_rules, is_required, is_active, display_order. Global fields apply to every list.