Campaigns API
Every Campaigns action (16) and data model (7): endpoints, scopes, input, preconditions, events and examples.
Email, SMS and WhatsApp campaigns to your customers, with opens and clicks.
Module code campaigns · 16 actions · 7 data models · 12 events. A key or token never does more here than the person behind it can; row-level security applies exactly as in the app. Scopes: campaigns (everything), campaigns:read (read only) or campaigns:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
audience_facts | audience_fact | /api/v1/audience_facts | |||
audiences | audience | /api/v1/audiences | campaigns.create_audience | campaigns.update_audience | campaigns.delete_audience |
campaign_clicks | campaign_click | /api/v1/campaign_clicks | |||
campaign_links | campaign_link | /api/v1/campaign_links | |||
campaign_recipients | campaign_recipient | /api/v1/campaign_recipients | |||
campaigns | campaign | /api/v1/campaigns | campaigns.create | campaigns.update | campaigns.delete |
marketing_unsubscribes | marketing_unsubscribe | /api/v1/marketing_unsubscribes |
Lists take limit (up to 200), cursor, order (e.g. -created_at), fields and q; every other query parameter is a column filter: ?status=paid, ?total[gte]=100, ?id[in]=a,b, ?x=null.
Actions
| Action | Needs | What it does |
|---|---|---|
campaigns.cancel | campaigns:write | Stop sending |
campaigns.create | campaigns:write | Draft a campaign |
campaigns.create_audience | campaigns:write | Create an audience |
campaigns.delete | campaigns:delete | Delete a draft campaign |
campaigns.delete_audience | campaigns:delete | Delete an audience |
campaigns.record_batch | campaigns:handle | Record a sent batch |
campaigns.resubscribe | campaigns:manage | Take off the unsubscribe list |
campaigns.resubscribe_me | campaigns:write | Send me campaign messages again |
campaigns.schedule | campaigns:write | Schedule or send |
campaigns.send_test | campaigns:write | Send a test |
campaigns.start | campaigns:handle | Start a due campaign |
campaigns.unschedule | campaigns:write | Back to draft |
campaigns.unsubscribe | campaigns:write | Unsubscribe an address |
campaigns.unsubscribe_me | campaigns:write | Stop campaign messages to me |
campaigns.update | campaigns:write | Edit a campaign |
campaigns.update_audience | campaigns:write | Edit an audience |
campaigns.cancel
Stop sending — Stops a scheduled or sending campaign; whoever has not been sent to yet is not.
Needs campaigns:write · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/campaigns/{id}/actions/cancel- Canonical address:
POST /api/v1/actions/campaigns.cancel
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Campaign · id of campaign |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
campaign_not_found | The campaign must exist. |
campaign_not_cancellable | Only a scheduled or sending campaign is stopped. |
Events it publishes: campaigns.cancelled
What it writes: campaign (update) — Marks it cancelled; campaign_recipient (update) — Skips those not yet sent to
curl -X POST https://app.getera.io/api/v1/campaigns/CAMPAIGN_ID/actions/cancel \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"campaigns.cancelled"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.campaigns.cancel("CAMPAIGN_ID")era campaigns cancel CAMPAIGN_ID{
"name": "era__campaigns__cancel",
"arguments": {
"id": "CAMPAIGN_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold campaigns:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 campaign_not_found, campaign_not_cancellable · 429 slow down
campaigns.create
Draft a campaign — An email, SMS or WhatsApp message to an audience, drafted to be tested, scheduled and sent.
Needs campaigns:write · changes data
https://app.getera.io/api/v1/campaigns/actions/create- REST alias:
POST /api/v1/campaigns - Canonical address:
POST /api/v1/actions/campaigns.create
Input
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Name · max 200 |
channel | string (enum) | Send by · Allowed values: email, sms, whatsapp | |
format | string (enum) | Format · Allowed values: text, html | |
subject | string | Subject · max 300 | |
subjectB | string | Subject B (A/B test) · max 300 | |
preheader | string | Preview line · max 200 | |
body | string | Message · max 20000 | |
blocks | object / array | Email blocks | |
audienceId | uuid | yes | Audience · id of audience |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
audience_not_found | The audience must exist. |
invalid_campaign_blocks | The email layout must be a list of known blocks with valid links. |
Events it publishes: campaigns.created
What it writes: campaign (insert) — Writes the draft
curl -X POST https://app.getera.io/api/v1/campaigns/actions/create \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name","audienceId":"AUDIENCE_ID"}'{
"ok": true,
"result": "…",
"events": [
"campaigns.created"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.campaigns.create({
name: "Name",
audienceId: "AUDIENCE_ID"
})era campaigns create \
--name "Name" \
--audience-id "AUDIENCE_ID"{
"name": "era__campaigns__create",
"arguments": {
"name": "Name",
"audienceId": "AUDIENCE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold campaigns:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 audience_not_found, invalid_campaign_blocks · 429 slow down
campaigns.create_audience
Create an audience — A saved segment of customers: their fields, their tags, whether they are a lead, and what they bought or booked.
Needs campaigns:write · changes data
https://app.getera.io/api/v1/audiences/actions/create_audience- REST alias:
POST /api/v1/audiences - Canonical address:
POST /api/v1/actions/campaigns.create_audience
Input
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Name · max 200 |
description | string | Description · max 2000 | |
match | string (enum) | Match · Allowed values: all, any | |
rules | object / array | Rules |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
invalid_audience_rules | Every rule needs a known field, operator and value. |
Events it publishes: campaigns.audience_changed
What it writes: audience (insert) — Saves the segment
curl -X POST https://app.getera.io/api/v1/audiences/actions/create_audience \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"campaigns.audience_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.audiences.create({
name: "Name"
})era audiences create \
--name "Name"{
"name": "era__campaigns__create_audience",
"arguments": {
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold campaigns:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 invalid_audience_rules · 429 slow down
campaigns.delete
Delete a draft campaign — Removes a campaign that was never sent.
Needs campaigns:delete · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/campaigns/{id}/actions/delete- REST alias:
DELETE /api/v1/campaigns/{id} - Canonical address:
POST /api/v1/actions/campaigns.delete
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Campaign · id of campaign |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
campaign_not_found | The campaign must exist. |
campaign_not_draft | Only a draft is changed. |
Events it publishes: campaigns.deleted
What it writes: campaign (delete) — Deletes the draft
curl -X POST https://app.getera.io/api/v1/campaigns/CAMPAIGN_ID/actions/delete \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"campaigns.deleted"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.campaigns.delete("CAMPAIGN_ID")era campaigns delete CAMPAIGN_ID{
"name": "era__campaigns__delete",
"arguments": {
"id": "CAMPAIGN_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold campaigns:delete · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 campaign_not_found, campaign_not_draft · 429 slow down
campaigns.delete_audience
Delete an audience — Removes a segment no campaign uses.
Needs campaigns:delete · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/audiences/{id}/actions/delete_audience- REST alias:
DELETE /api/v1/audiences/{id} - Canonical address:
POST /api/v1/actions/campaigns.delete_audience
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Audience · id of audience |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
audience_not_found | The audience must exist. |
audience_in_use | An audience a campaign uses stays. |
Events it publishes: campaigns.audience_changed
What it writes: audience (delete) — Deletes the segment
curl -X POST https://app.getera.io/api/v1/audiences/AUDIENCE_ID/actions/delete_audience \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"campaigns.audience_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.audiences.deleteAudience("AUDIENCE_ID")era audiences delete_audience AUDIENCE_ID{
"name": "era__campaigns__delete_audience",
"arguments": {
"id": "AUDIENCE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold campaigns:delete · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 audience_not_found, audience_in_use · 429 slow down
campaigns.record_batch
Record a sent batch — Machine-only (campaigns-jobs.ts): which recipients were handed to the sender and which were skipped.
Needs campaigns:handle · changes data
403 machine_only).https://app.getera.io/api/v1/campaigns/{id}/actions/record_batch- Canonical address:
POST /api/v1/actions/campaigns.record_batch
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Campaign · id of campaign |
entries | object / array | yes | Entries |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
campaign_not_found | The campaign must exist. |
invalid_batch | The batch must be a list. |
Events it publishes: campaigns.batch_recorded, campaigns.sent
What it writes: campaign_recipient (update) — Queued or skipped; campaign (update) — Sent when nobody is left
Errors: 401 no or expired credential · 403 the credential does not hold campaigns:handle · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 campaign_not_found, invalid_batch · 429 slow down
campaigns.resubscribe
Take off the unsubscribe list — For a person who asked to hear from you again.
Needs campaigns:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/marketing_unsubscribes/actions/resubscribe- Canonical address:
POST /api/v1/actions/campaigns.resubscribe
Input
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | yes | Email or phone · max 320 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
unsubscribe_not_found | The address must be on the list. |
Events it publishes: campaigns.resubscribed
What it writes: marketing_unsubscribe (delete) — Removes the address
curl -X POST https://app.getera.io/api/v1/marketing_unsubscribes/actions/resubscribe \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"email":"Email or phone"}'{
"ok": true,
"result": "…",
"events": [
"campaigns.resubscribed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.marketingUnsubscribes.resubscribe({
email: "Email or phone"
})era marketing_unsubscribes resubscribe \
--email "Email or phone"{
"name": "era__campaigns__resubscribe",
"arguments": {
"email": "Email or phone"
}
}Errors: 401 no or expired credential · 403 the credential does not hold campaigns:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 unsubscribe_not_found · 429 slow down
campaigns.resubscribe_me
Send me campaign messages again — A signed-in customer turns campaign emails and texts back on from their site account.
Needs campaigns:write · removes or cannot be undone · Also callable from a published site (customer)
/api/site/v1/{siteId}, e.g. with @getera/storefront). With an API key or a token it is refused (403 site_only).https://app.getera.io/api/v1/marketing_unsubscribes/actions/resubscribe_me- Canonical address:
POST /api/v1/actions/campaigns.resubscribe_me
Input
This action takes no input.
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_contact_on_record | The customer’s record needs an email or a phone. |
unsubscribe_not_found | Only when they are on the list. |
Events it publishes: campaigns.resubscribed
What it writes: marketing_unsubscribe (delete) — Their own address off the list
Errors: 401 no or expired credential · 403 the credential does not hold campaigns:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_contact_on_record, unsubscribe_not_found · 429 slow down
campaigns.schedule
Schedule or send — Sends at the chosen time, or now; it goes out in batches through notifications.
Needs campaigns:write · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/campaigns/{id}/actions/schedule- Canonical address:
POST /api/v1/actions/campaigns.schedule
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Campaign · id of campaign |
sendAt | date-time | Send at |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
campaign_not_found | The campaign must exist. |
campaign_not_draft | Only a draft is changed. |
campaign_incomplete | A message (and for email a subject) is needed; SMS is at most 1500 characters. |
campaign_no_channel | An email campaign needs an active email channel. |
audience_empty | The audience must reach at least one address. |
Events it publishes: campaigns.scheduled
What it writes: campaign (update) — Marks it scheduled
curl -X POST https://app.getera.io/api/v1/campaigns/CAMPAIGN_ID/actions/schedule \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"sendAt":"2026-11-02T09:00:00Z"}'{
"ok": true,
"result": "…",
"events": [
"campaigns.scheduled"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.campaigns.schedule("CAMPAIGN_ID", {
sendAt: "2026-11-02T09:00:00Z"
})era campaigns schedule CAMPAIGN_ID \
--send-at "2026-11-02T09:00:00Z"{
"name": "era__campaigns__schedule",
"arguments": {
"id": "CAMPAIGN_ID",
"sendAt": "2026-11-02T09:00:00Z"
}
}Errors: 401 no or expired credential · 403 the credential does not hold campaigns:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 campaign_not_found, campaign_not_draft, campaign_incomplete, campaign_no_channel, audience_empty · 429 slow down
campaigns.send_test
Send a test — One copy to one address, through the same email channel the campaign will use — the designed email as it goes out.
Needs campaigns:write · reaches outside (sends, publishes or charges)
https://app.getera.io/api/v1/campaigns/{id}/actions/send_test- Canonical address:
POST /api/v1/actions/campaigns.send_test
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Campaign · id of campaign |
to | string | yes | Send to · max 320 |
variant | string (enum) | Subject · Allowed values: a, b |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
campaign_not_found | The campaign must exist. |
invalid_recipient | The address must be an email address. |
Events it publishes: campaigns.test_requested
What it writes: campaign (update) — Stamps when a test last went
curl -X POST https://app.getera.io/api/v1/campaigns/CAMPAIGN_ID/actions/send_test \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"to":"Send to"}'{
"ok": true,
"result": "…",
"events": [
"campaigns.test_requested"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.campaigns.sendTest("CAMPAIGN_ID", {
to: "Send to"
})era campaigns send_test CAMPAIGN_ID \
--to "Send to"{
"name": "era__campaigns__send_test",
"arguments": {
"id": "CAMPAIGN_ID",
"to": "Send to"
}
}Errors: 401 no or expired credential · 403 the credential does not hold campaigns:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 campaign_not_found, invalid_recipient · 429 slow down
campaigns.start
Start a due campaign — Machine-only (campaigns-jobs.ts): lists the recipients, registers the links to track, and starts sending.
Needs campaigns:handle · changes data
403 machine_only).https://app.getera.io/api/v1/campaigns/{id}/actions/start- Canonical address:
POST /api/v1/actions/campaigns.start
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Campaign · id of campaign |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
campaign_not_found | The campaign must exist. |
campaign_not_due | Only a scheduled campaign whose time came starts. |
Events it publishes: campaigns.started, campaigns.sent
What it writes: campaign_recipient (insert) — One row per address; campaign_link (insert) — One row per link in the email, for click tracking; campaign (update) — Marks it sending
Errors: 401 no or expired credential · 403 the credential does not hold campaigns:handle · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 campaign_not_found, campaign_not_due · 429 slow down
campaigns.unschedule
Back to draft — Takes a scheduled campaign back to a draft before it starts.
Needs campaigns:write · changes data
https://app.getera.io/api/v1/campaigns/{id}/actions/unschedule- Canonical address:
POST /api/v1/actions/campaigns.unschedule
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Campaign · id of campaign |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
campaign_not_found | The campaign must exist. |
campaign_not_scheduled | Only a scheduled campaign is unscheduled. |
Events it publishes: campaigns.updated
What it writes: campaign (update) — Back to draft
curl -X POST https://app.getera.io/api/v1/campaigns/CAMPAIGN_ID/actions/unschedule \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"campaigns.updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.campaigns.unschedule("CAMPAIGN_ID")era campaigns unschedule CAMPAIGN_ID{
"name": "era__campaigns__unschedule",
"arguments": {
"id": "CAMPAIGN_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold campaigns:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 campaign_not_found, campaign_not_scheduled · 429 slow down
campaigns.unsubscribe
Unsubscribe an address — Puts an address or phone number on the unsubscribe list — from the message’s link, or by a member on request.
Needs campaigns:write · changes data · Also callable from a published site (visitor)
/api/site/v1/{siteId}, e.g. with @getera/storefront). With an API key or a token it is refused (403 site_only).https://app.getera.io/api/v1/marketing_unsubscribes/actions/unsubscribe- Canonical address:
POST /api/v1/actions/campaigns.unsubscribe
Input
| Field | Type | Required | Notes |
|---|---|---|---|
token | string | Link token · max 100 | |
email | string | Email · max 320 | |
phone | string | Phone · max 40 | |
reason | string | Reason · max 1000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
unsubscribe_link_invalid | A link token must be one this workspace sent. |
unsubscribe_link_required | From the site, only the link from the message unsubscribes. |
invalid_recipient | An address must be an email address or a phone number. |
Events it publishes: campaigns.unsubscribed
What it writes: marketing_unsubscribe (insert) — Adds the address to the list
Errors: 401 no or expired credential · 403 the credential does not hold campaigns:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 unsubscribe_link_invalid, unsubscribe_link_required, invalid_recipient · 429 slow down
campaigns.unsubscribe_me
Stop campaign messages to me — A signed-in customer turns off campaign emails and texts from their site account.
Needs campaigns:write · changes data · Also callable from a published site (customer)
/api/site/v1/{siteId}, e.g. with @getera/storefront). With an API key or a token it is refused (403 site_only).https://app.getera.io/api/v1/marketing_unsubscribes/actions/unsubscribe_me- Canonical address:
POST /api/v1/actions/campaigns.unsubscribe_me
Input
This action takes no input.
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_contact_on_record | The customer’s record needs an email or a phone. |
already_unsubscribed | Not when they are already off. |
Events it publishes: campaigns.unsubscribed
What it writes: marketing_unsubscribe (insert) — Their own address on the list
Errors: 401 no or expired credential · 403 the credential does not hold campaigns:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_contact_on_record, already_unsubscribed · 429 slow down
campaigns.update
Edit a campaign — Changes a draft’s name, channel, subject, message, email blocks or audience.
Needs campaigns:write · changes data
https://app.getera.io/api/v1/campaigns/{id}/actions/update- REST alias:
PATCH /api/v1/campaigns/{id} - Canonical address:
POST /api/v1/actions/campaigns.update
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Campaign · id of campaign |
name | string | Name · max 200 | |
channel | string (enum) | Send by · Allowed values: email, sms, whatsapp | |
format | string (enum) | Format · Allowed values: text, html | |
subject | string | Subject · max 300 | |
subjectB | string | Subject B (A/B test) · max 300 | |
preheader | string | Preview line · max 200 | |
body | string | Message · max 20000 | |
blocks | object / array | Email blocks | |
audienceId | uuid | Audience · id of audience |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
campaign_not_found | The campaign must exist. |
campaign_not_draft | Only a draft is changed. |
audience_not_found | The audience must exist. |
invalid_campaign_blocks | The email layout must be a list of known blocks with valid links. |
Events it publishes: campaigns.updated
What it writes: campaign (update) — Rewrites the draft
curl -X POST https://app.getera.io/api/v1/campaigns/CAMPAIGN_ID/actions/update \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"campaigns.updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.campaigns.update("CAMPAIGN_ID", {
name: "Name"
})era campaigns update CAMPAIGN_ID \
--name "Name"{
"name": "era__campaigns__update",
"arguments": {
"id": "CAMPAIGN_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold campaigns:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 campaign_not_found, campaign_not_draft, audience_not_found, invalid_campaign_blocks · 429 slow down
campaigns.update_audience
Edit an audience — Changes the segment’s name or rules. Campaigns already sent keep who they were sent to.
Needs campaigns:write · changes data
https://app.getera.io/api/v1/audiences/{id}/actions/update_audience- REST alias:
PATCH /api/v1/audiences/{id} - Canonical address:
POST /api/v1/actions/campaigns.update_audience
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Audience · id of audience |
name | string | Name · max 200 | |
description | string | Description · max 2000 | |
match | string (enum) | Match · Allowed values: all, any | |
rules | object / array | Rules |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
audience_not_found | The audience must exist. |
invalid_audience_rules | Every rule needs a known field, operator and value. |
Events it publishes: campaigns.audience_changed
What it writes: audience (update) — Rewrites the segment
curl -X POST https://app.getera.io/api/v1/audiences/AUDIENCE_ID/actions/update_audience \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"campaigns.audience_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.audiences.updateAudience("AUDIENCE_ID", {
name: "Name"
})era audiences update_audience AUDIENCE_ID \
--name "Name"{
"name": "era__campaigns__update_audience",
"arguments": {
"id": "AUDIENCE_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold campaigns:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 audience_not_found, invalid_audience_rules · 429 slow down
Data models
AudienceFact
Table audience_fact · Resource audience_facts
GET /api/v1/audience_factsGET /api/v1/audience_facts/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
customer_id | string (uuid) · uuid | customer | ||
kind | string · text | order, visit, package | ||
source_id | string · text | |||
state | string · text | open, done, cancelled | ||
occurred_on | string (date) · date | yes | ||
active_until | string (date) · date | yes | ||
amount | string (int64) · int8 | |||
currency | string · bpchar | yes | max 3 | |
event_id | string (int64) · int8 | yes | ||
updated_at | string (date-time) · timestamptz |
Primary key: id
Unique: workspace_id, kind, source_id
Checks:
CHECK ((amount >= 0))CHECK (((currency IS NULL) OR (currency ~ '^[A-Z]{3}$'::text)))
Audience
Table audience · Resource audiences
GET /api/v1/audiencesGET /api/v1/audiences/{id}POST /api/v1/audiences→campaigns.create_audiencePATCH /api/v1/audiences/{id}→campaigns.update_audienceDELETE /api/v1/audiences/{id}→campaigns.delete_audience
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
name | string · text | |||
description | string · text | yes | ||
match | string · text | all, any | ||
rules | object · jsonb | |||
created_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((length(TRIM(BOTH FROM name)) > 0))CHECK ((jsonb_typeof(rules) = 'array'::text))
CampaignClick
Table campaign_click · Resource campaign_clicks
GET /api/v1/campaign_clicksGET /api/v1/campaign_clicks/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
campaign_id | string (uuid) · uuid | campaign | ||
link_id | string (uuid) · uuid | campaign_link | ||
recipient_id | string (uuid) · uuid | yes | campaign_recipient | |
occurred_at | string (date-time) · timestamptz |
Primary key: id
CampaignLink
Table campaign_link · Resource campaign_links
GET /api/v1/campaign_linksGET /api/v1/campaign_links/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
campaign_id | string (uuid) · uuid | campaign | ||
url | string · text | |||
created_at | string (date-time) · timestamptz |
Primary key: id
Unique: campaign_id, url
Checks:
CHECK ((url ~* '^(https?://\|mailto:\|tel:)'::text))
CampaignRecipient
Table campaign_recipient · Resource campaign_recipients
GET /api/v1/campaign_recipientsGET /api/v1/campaign_recipients/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
campaign_id | string (uuid) · uuid | campaign | ||
customer_id | string (uuid) · uuid | yes | customer | |
email | string · text | yes | ||
name | string · text | yes | ||
state | string · text | pending, queued, skipped | ||
skip_reason | string · text | yes | ||
delivery_id | string (uuid) · uuid | yes | notification_delivery | |
queued_at | string (date-time) · timestamptz | yes | ||
created_at | string (date-time) · timestamptz | |||
phone | string · text | yes | ||
variant | string · bpchar | a, b | ||
opened_at | string (date-time) · timestamptz | yes | ||
open_count | integer · int4 | |||
clicked_at | string (date-time) · timestamptz | yes | ||
click_count | integer · int4 |
Primary key: id
Unique: campaign_id, email
Checks:
CHECK (((email IS NOT NULL) OR (phone IS NOT NULL)))
Campaign
Table campaign · Resource campaigns
GET /api/v1/campaignsGET /api/v1/campaigns/{id}POST /api/v1/campaigns→campaigns.createPATCH /api/v1/campaigns/{id}→campaigns.updateDELETE /api/v1/campaigns/{id}→campaigns.delete
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
name | string · text | |||
subject | string · text | |||
body | string · text | |||
audience_id | string (uuid) · uuid | audience | ||
status | string · text | draft, scheduled, sending, sent, cancelled | ||
scheduled_at | string (date-time) · timestamptz | yes | ||
started_at | string (date-time) · timestamptz | yes | ||
completed_at | string (date-time) · timestamptz | yes | ||
cancelled_at | string (date-time) · timestamptz | yes | ||
recipient_count | integer · int4 | |||
test_sent_at | string (date-time) · timestamptz | yes | ||
created_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
channel | string · text | email, sms, whatsapp | ||
format | string · text | text, html | ||
blocks | object · jsonb | |||
preheader | string · text | yes | ||
subject_b | string · text | yes |
Primary key: id
Checks:
CHECK ((jsonb_typeof(blocks) = 'array'::text))CHECK ((length(TRIM(BOTH FROM name)) > 0))CHECK (((status <> 'scheduled'::text) OR (scheduled_at IS NOT NULL)))
MarketingUnsubscribe
Table marketing_unsubscribe · Resource marketing_unsubscribes
GET /api/v1/marketing_unsubscribesGET /api/v1/marketing_unsubscribes/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
email | string · text | yes | ||
customer_id | string (uuid) · uuid | yes | customer | |
campaign_id | string (uuid) · uuid | yes | campaign | |
source | string · text | link, manual | ||
reason | string · text | yes | ||
created_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
phone | string · text | yes |
Primary key: id
Unique: workspace_id, email
Checks:
CHECK (((email IS NOT NULL) OR (phone IS NOT NULL)))CHECK ((email = lower(email)))
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
campaigns.audience_changed | An audience was created, edited or deleted. | id |
campaigns.batch_recorded | A batch of a campaign was handed to the sender. | id, handled, pending |
campaigns.cancelled | A campaign was stopped. | id, notSent, ownerUserId |
campaigns.created | A campaign was drafted. | id, ownerUserId |
campaigns.deleted | A draft campaign was deleted. | id |
campaigns.resubscribed | An address was taken off the unsubscribe list. | email, customerId |
campaigns.scheduled | A campaign was scheduled. | id, scheduledAt, audienceSize, ownerUserId |
campaigns.sent | Every recipient of a campaign was handed to the sender. | id, recipientCount, ownerUserId |
campaigns.started | A campaign started sending. | id, recipientCount |
campaigns.test_requested | A test copy was asked for; notifications delivers it. | id, to, subject, text, html, sourceId |
campaigns.unsubscribed | An address unsubscribed. | email, phone, customerId, campaignId, source |
campaigns.updated | A draft campaign was edited or unscheduled. | id |