Marketing
Package marketing is lifecycle email: drip sequences that reach the right people.
Package marketing is lifecycle email: drip sequences that reach the right people.
| Base URL | https://api.hanzo.ai |
| Operations | 35 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
marketing
GET /v1/marketing/audiences/{id}/preview
Evaluates the cohort LIVE — the same resolution an enrollment would run — and reports how big it is and how many real mailboxes it reaches. It is the honest answer to "is this send worth making": a cohort of 500 that mails 3 says so, in deliverable and unmatched. Nothing is sent.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the audience id from the path, as returned by create. |
GET /v1/marketing/audiences/{id}
Returns one of the caller org's saved audiences. An audience belonging to another org reads as not found.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the audience id from the path, as returned by create. |
DELETE /v1/marketing/audiences/{id}
Removes one of the caller org's audiences and answers 204. It deletes the saved filter only — no customer, event or enrollment is touched.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the audience id from the path, as returned by create. |
GET /v1/marketing/audiences
Returns the org's saved audiences, most recently updated first.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | — | Limit caps the rows returned; 0 means 200 and nothing above 1000 is honoured. |
POST /v1/marketing/audiences
Saves a cohort filter for the caller's org. Name is required. Omitting event saves the WHOLE-ORG audience — every mailable customer — which needs no analytics warehouse; naming one narrows that roster to the customers who fired it within windowDays.
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
createdAt | integer | — | CreatedAt and UpdatedAt are unix seconds, both server-assigned. |
event | string | — | Event is the analytics event a member must have fired. |
id | string | — | ID is the server-assigned audience id ("aud_" + 128 random bits). |
name | string | — | Name is the audience's label. |
updatedAt | integer | — | |
windowDays | integer | — | WindowDays is how far back the event counts, ending now. 0 means 30 and nothing above 3650 is honoured. |
POST /v1/marketing/calendar/{id}/publish
Publishes a post NOW, synchronously, whatever its schedule. No social connector is wired today, so every channel answers an honest 501 naming the seam a real one would plug into, and the post is recorded failed with that exact reason — never a faked "published".
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the post id from the path, as returned by create. |
GET /v1/marketing/calendar/{id}
Returns one of the caller org's posts, including the exact error behind a failed publish. A post belonging to another org reads as not found.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the post id from the path, as returned by create. |
PUT /v1/marketing/calendar/{id}
Replaces a post's editable fields. It is a full write, not a patch, and it RESETS the lifecycle from the schedule: a scheduledAt makes the post "scheduled" again and none makes it a draft — so editing a failed post requeues it rather than leaving it stuck.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the server-assigned post id ("cal_" + 128 random bits). |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
body | string | — | Body is the post text. |
channel | string | — | Channel is the target network: x, facebook, instagram, linkedin, tiktok, youtube or threads. |
createdAt | integer | — | CreatedAt and UpdatedAt are unix seconds, both server-assigned. |
error | string | — | Error is the exact reason the last publish attempt failed — the honest record behind a "failed" status, never a faked… |
id | string | — | ID is the server-assigned post id ("cal_" + 128 random bits). |
publishedAt | integer | — | PublishedAt is when the publish succeeded; 0 until it does. |
scheduledAt | integer | — | ScheduledAt is the unix publish time; 0 leaves the post a draft, and any value makes it "scheduled" for the durable… |
status | string | — | Status is draft, scheduled, published, failed or canceled. |
title | string | — | Title is the post's internal label, capped at 1024 bytes. |
updatedAt | integer | — |
DELETE /v1/marketing/calendar/{id}
Removes one of the caller org's posts and answers 204. A post already published is deleted from the calendar only — nothing is retracted from the network it went out on.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the post id from the path, as returned by create. |
GET /v1/marketing/calendar
Returns the org's calendar, soonest scheduled first, optionally narrowed to one status.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
status | query | string | — | Status keeps only posts in that state (draft, scheduled, published, failed, canceled). |
limit | query | integer | — | Limit caps the rows returned; 0 means 200 and nothing above 1000 is honoured. |
POST /v1/marketing/calendar
Adds a post to the content calendar. Channel and body are required. A scheduledAt in the future makes the post "scheduled" and the durable sweep publishes it when it comes due — claimed once, so a post publishes at most once; without one it stays a draft.
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
body | string | — | Body is the post text. |
channel | string | — | Channel is the target network: x, facebook, instagram, linkedin, tiktok, youtube or threads. |
createdAt | integer | — | CreatedAt and UpdatedAt are unix seconds, both server-assigned. |
error | string | — | Error is the exact reason the last publish attempt failed — the honest record behind a "failed" status, never a faked… |
id | string | — | ID is the server-assigned post id ("cal_" + 128 random bits). |
publishedAt | integer | — | PublishedAt is when the publish succeeded; 0 until it does. |
scheduledAt | integer | — | ScheduledAt is the unix publish time; 0 leaves the post a draft, and any value makes it "scheduled" for the durable… |
status | string | — | Status is draft, scheduled, published, failed or canceled. |
title | string | — | Title is the post's internal label, capped at 1024 bytes. |
updatedAt | integer | — |
POST /v1/marketing/campaigns/{id}/schedule
Sets a campaign's send time and moves it to "scheduled". A scheduledAt of 0 clears the schedule and returns it to "draft".
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the campaign id from the path. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | — | ID is the campaign id from the path. |
scheduledAt | integer | — | ScheduledAt is the unix send time. |
GET /v1/marketing/campaigns/{id}
Returns one of the caller org's campaigns. A campaign belonging to another org reads as not found.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the campaign id from the path, as returned by create. |
PUT /v1/marketing/campaigns/{id}
Replaces a campaign's editable fields. It is a full write, not a patch: every field takes the value in the body, and an omitted one is cleared. The id comes from the path — the body cannot retarget another campaign — and createdAt is never rewritten.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the server-assigned campaign id ("camp_" + 128 random bits). |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
budget | integer | — | Budget and Spend are minor units (USD cents), clamped to >= 0. |
channel | string | — | Channel is the delivery surface: email, sms, social, meta, google or tiktok. |
createdAt | integer | — | CreatedAt and UpdatedAt are unix seconds, both server-assigned. |
id | string | — | ID is the server-assigned campaign id ("camp_" + 128 random bits). |
name | string | — | Name is the campaign's label. |
objective | string | — | Objective is the free-text goal ("signups"), capped at 1024 bytes. |
scheduledAt | integer | — | ScheduledAt is the unix send time; 0 means unscheduled. |
spend | integer | — | |
status | string | — | Status is the lifecycle: draft, scheduled, active, paused or completed. |
updatedAt | integer | — |
DELETE /v1/marketing/campaigns/{id}
Removes one of the caller org's campaigns and answers 204. A campaign belonging to another org reads as not found and is left untouched.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the campaign id from the path, as returned by create. |
GET /v1/marketing/campaigns
Returns the org's campaigns, most recently updated first, optionally narrowed to one lifecycle status.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
status | query | string | — | Status keeps only campaigns in that lifecycle state (draft, scheduled, active, paused, completed). |
limit | query | integer | — | Limit caps the rows returned; 0 means 200 and nothing above 1000 is honoured. |
POST /v1/marketing/campaigns
Registers a campaign in the caller's org. Name is required; channel defaults to email and status to draft, and a future scheduledAt with no explicit status makes the campaign "scheduled". Budget and spend are cents and are clamped to >= 0. The id, createdAt and updatedAt of the input are ignored — the server assigns them.
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
budget | integer | — | Budget and Spend are minor units (USD cents), clamped to >= 0. |
channel | string | — | Channel is the delivery surface: email, sms, social, meta, google or tiktok. |
createdAt | integer | — | CreatedAt and UpdatedAt are unix seconds, both server-assigned. |
id | string | — | ID is the server-assigned campaign id ("camp_" + 128 random bits). |
name | string | — | Name is the campaign's label. |
objective | string | — | Objective is the free-text goal ("signups"), capped at 1024 bytes. |
scheduledAt | integer | — | ScheduledAt is the unix send time; 0 means unscheduled. |
spend | integer | — | |
status | string | — | Status is the lifecycle: draft, scheduled, active, paused or completed. |
updatedAt | integer | — |
GET /v1/marketing/promos/{code}/eligibility
Prices a promo against a plan and seat count. It is PURE: nothing is redeemed, credited or counted, so it is safe to call from a pricing page on every keystroke. An inactive promo or an exhausted cap quotes ineligible with the reason rather than erroring.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
code | path | string | yes | Code is the promo code from the path. |
plan | query | string | — | Plan is the plan being priced: pro, max or team. |
seats | query | integer | — | Seats is the Team seat count; 0 means 1, and it is ignored for the single-seat plans. |
POST /v1/marketing/promos/{code}/redeem
Records the caller org's claim on a promo. NOTHING IS CREDITED: the redemption is a row, and credit into an org is an admin decision made on the admin surface against an auditable ledger.
The plan is DERIVED from the org's live ACTIVE/TRIALING paid subscription and can never be named by the caller — an org with no qualifying subscription is refused, and so is one whose subscription cannot be read. The seat count is the single-seat floor (claimSeats), so the recorded figure has no input that can inflate it.
Guards run under one lock so the cap cannot be raced past: the fleet-wide redemption cap, one redemption per org, one per payment instrument (REQUIRED), and the per-redemption ceiling.
It is IDEMPOTENT: an org that already redeemed gets its original redemption back with alreadyRedeemed true.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
code | path | string | yes | Code is the promo code from the path. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
code | string | — | Code is the promo code from the path. |
instrument | string | — | Instrument identifies the payment method. It is the anti-farming key: one redemption per instrument, fleet-wide, and it… |
GET /v1/marketing/promos/{code}/redemption
Returns the caller org's OWN redemption of a promo — an org-scoped read, so it can never surface another tenant's. Not found when this org has not redeemed it.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
code | path | string | yes | Code is the promo code from the path, e.g. |
GET /v1/marketing/promos
Returns every promo the deployment offers with its live counters: how many orgs have redeemed it and how many redemptions remain under the cap. The promos are fleet-wide, not per-org — only the counters move.
POST /v1/marketing/sequences/{id}/enroll
Adds one contact or a whole audience to a sequence and schedules the first step for each. The sequence must be ACTIVE (a draft sends nothing), and the request must name exactly one of address or audienceId.
Enrolling is ALL this does: the message itself is sent later by the drip engine, through the suppression gate, so an opted-out customer can be enrolled here and still never be mailed. Re-posting is safe — an address this sequence already took is counted in alreadyEnrolled and never double-dripped — which is what makes retrying a partially-applied announcement a resume rather than a second send.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the sequence id from the path. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
address | string | — | Address is a single recipient, normalized (lower-cased, trimmed) before use. |
audienceId | string | — | AudienceID fans the sequence out over a saved audience, resolved live to the org's mailable customers. |
channel | string | — | Channel is the delivery surface; empty means email. |
id | string | — | ID is the sequence id from the path. |
POST /v1/marketing/sequences/{id}/enrollments/{eid}/cancel
Stops one walk mid-sequence and answers 204: no further step is sent, and steps already delivered are not recalled. Only an ACTIVE enrollment can be canceled — one already completed or canceled reads as not found.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the sequence id from the path. |
eid | path | string | yes | EID is the enrollment id from the path, as returned by a single-address enroll. |
GET /v1/marketing/sequences/{id}/enrollments
Returns who is walking one sequence, most recently enrolled first, with each walk's current step and next due time.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the sequence id from the path. |
limit | query | integer | — | Limit caps the rows returned; 0 means 200 and nothing above 1000 is honoured. |
POST /v1/marketing/sequences/{id}/status
Flips draft/active/archived — the activation gate for sending, since only an active sequence accepts enrollments. It does not touch enrollments already walking: archiving stops new ones, not in-flight ones.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the sequence id from the path. |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | — | ID is the sequence id from the path. |
status | string | — | Status is draft, active or archived. Required; there is no default here, unlike on create. |
GET /v1/marketing/sequences/{id}/steps
Returns one sequence's steps in send order.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the sequence id from the path, as returned by create. |
POST /v1/marketing/sequences/{id}/steps
Appends a message to the END of a sequence: the new step's idx is one past the last, so steps arrive in the order they are added. Body is required and delaySeconds must be >= 0. Adding a step does not disturb enrollments already walking — one that has passed this index simply never sees it.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | SequenceID is the sequence id from the path (the route's :id). |
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
body | string | — | Body is the message text. |
delaySeconds | integer | — | DelaySeconds is how long after the previous step this one sends (after enrollment, for the first step). |
id | string | — | SequenceID is the sequence id from the path (the route's :id). |
subject | string | — | Subject is the email subject line, capped at 1024 bytes. |
GET /v1/marketing/sequences/{id}
Returns one of the caller org's sequences together with its steps in send order. A sequence belonging to another org reads as not found.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | ID is the sequence id from the path, as returned by create. |
GET /v1/marketing/sequences
Returns the org's drip sequences, most recently updated first.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | — | Limit caps the rows returned; 0 means 200 and nothing above 1000 is honoured. |
POST /v1/marketing/sequences
Registers a drip sequence in the caller's org. Name is required; status defaults to draft, and a sequence must be ACTIVE before it will accept enrollments. The id, createdAt and updatedAt of the input are ignored — the server assigns them.
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
createdAt | integer | — | CreatedAt and UpdatedAt are unix seconds, both server-assigned. |
id | string | — | ID is the server-assigned sequence id ("seq_" + 128 random bits). |
name | string | — | Name is the sequence's label. |
status | string | — | Status is the lifecycle: draft, active or archived. |
updatedAt | integer | — |
GET /v1/marketing/summary
Rolls up the caller org's campaigns: how many there are, how many are active, and the summed budget and spend in cents.
GET /v1/marketing/suppressions
Returns the org's opt-out list, newest first — everyone the send gate will refuse to deliver to.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | — | Limit caps the rows returned; 0 means 200 and nothing above 1000 is honoured. |
POST /v1/marketing/suppressions
Records an opt-out for the org (admin / self-service management). Address is required; channel defaults to email. It is idempotent: re-suppressing the same tuple keeps the original record rather than erroring. From here on the ONE send gate refuses that recipient on that channel.
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
address | string | — | Address is the recipient, normalized (lower-cased, trimmed) so an opt-out cannot be slipped past on a case or… |
channel | string | — | Channel is the surface opted out of: email, sms, social, meta, google or tiktok. Empty means email. |
createdAt | integer | — | CreatedAt is unix seconds, server-assigned. |
reason | string | — | Reason is a free-text note, capped at 1024 bytes. |
DELETE /v1/marketing/suppressions
Re-subscribes an address on one channel and answers 204. An address that is not on the list reads as not found.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
channel | query | string | — | Channel is the surface opted out of: email, sms, social, meta, google or tiktok. Empty means email. |
address | query | string | — | Address is the recipient, normalized (lower-cased, trimmed) so an opt-out cannot be slipped past on a case or… |
reason | query | string | — | Reason is a free-text note, capped at 1024 bytes. |
createdAt | query | integer | — | CreatedAt is unix seconds, server-assigned. |
GET /v1/marketing/unsubscribe
Is the PUBLIC one-click endpoint (no principal): a recipient clicks the signed link in an email footer. The token binds (org, channel, address), so a caller can only opt OUT exactly the tuple it was minted for — never another address and never another org. An invalid token is refused, and a deployment with no KMS-sealed key refuses rather than accepting anything.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
org | query | string | — | Org is the org the link was minted for. |
channel | query | string | — | Channel is the surface to opt out of. |
address | query | string | — | Address is the recipient to opt out. |
token | query | string | — | Token is the HMAC over (org, channel, address). |
How is this guide?