Journeys audience from your app
A Host app can put its people on a Journeys canvas, with each person's lawful basis for marketing, and take back off the ones it put there. A CRM uses it so that a Journeys campaign with the audience Members with consent mails the people the CRM holds consent for, and stops when the CRM withdraws them.
A membership is not permission to mail. Every send still checks, at that moment, that the membership is
subscribed with a basis that allows marketing, that the person has not opted out of all mail from your
organisation, and that the address is not on the organisation's suppression list.
Anyone reading memberships must apply the same checks.
The grant
One canvas per grant: journeys:audience:write:<canvas id>. Only an org admin can add it, and only for a
live canvas in the organisation. * does not include it.
waymaker host apps update <app-id> --commander-permissions "team_ids,journeys:audience:write:<canvas id>"
From an agent, use the MCP tool host_app_update with
commander_permissions: ["team_ids", "journeys:audience:write:<canvas id>"].
- The list replaces what the app had, so include the names it already holds.
- Removing it does not need an admin. The next call after removal is refused.
- To find a canvas id, read any campaign on that canvas with the MCP tool
commander_journey_get: it is the campaign'scanvas_id. Write it in lowercase.
Calling it
The app's own context key only. A signed-in person's token is refused (app_key_required).
POST https://apps.waymakerone.com/functions/v1/host-context-api:
{ "action": "journeys_audience_upsert", "data": { "canvas_id": "<canvas id>", "members": [
{ "email": "pat@example.com", "lawful_basis": "express_opt_in", "note": "ticked the box on the signup form" },
{ "email": "lee@example.com", "lawful_basis": "legitimate_interest_b2b", "contact_type": "business",
"basis_expires_at": "2027-11-01T00:00:00Z" }
] } }
{ "action": "journeys_audience_archive", "data": { "canvas_id": "<canvas id>", "members": [{ "email": "pat@example.com" }] } }
Both return { canvas_id, results: [{ email, status, reason? }], counts }, one result per member, in order.
Each member
| Field | |
|---|---|
email | Required. Lower-cased and trimmed. |
lawful_basis | Required on upsert: express_opt_in, enquiry, existing_relationship, legitimate_interest_b2b or contract. |
contact_type | business or consumer. legitimate_interest_b2b needs business. |
basis_expires_at | Optional. An ISO time in the future. After it, the person is not mailed. |
note | Optional, up to 200 characters: where the consent came from. |
Each upsert states the person's whole basis as your app holds it now. Leaving out contact_type or
basis_expires_at on a later upsert clears it on your membership, so send them every time.
What upsert does
status | Meaning |
|---|---|
created | The person is on the canvas with this basis. A contact is created if the organisation has none for the address; an existing contact's details are never changed. If that person has already opted out of your organisation's mail, the membership is recorded but not mailable, and the result still says created. |
updated | Your app's membership now has this basis. A membership your app archived is back in the audience, unless the person has opted out since. |
unchanged | Your app's membership already had exactly this. |
not_owned | Someone else put this person on the canvas: a person, an import or another app. Left as it is. |
invalid | Not written. reason: not_an_email, no_lawful_basis, b2b_basis_needs_business_contact, basis_expired, bad_expiry, bad_contact_type, note_too_long. |
What archive does
status | Meaning |
|---|---|
archived | The person is no longer in your app's audience on this canvas. It is not an unsubscribe: they did not opt out, and other lists are untouched. |
unchanged | Your app had already archived them. |
not_owned | Someone else's membership. Left as it is. |
not_found | The person is not on this canvas. |
invalid | Not an email address. |
Your app gets the same answers for its own members whatever their mail state: the results are identical whether or
not the person unsubscribed, bounced or is suppressed, and an upsert never makes such a person mailable again. If
your app needs mail state, give it journeys:suppressions:read and read the
suppression list, where unsubscribes and bounces are recorded.
Limits and errors
- At most 500 members a call (
too_many_members) and 5,000 per app per rolling hour, upserts and archives together. A call over the hour is refused whole with 429rate_limitedand aRetry-Afterheader. Nothing is written, so send the same batch again later. - 403
missing_grantnames the grant the app needs for that canvas. 403app_key_required: use the context key. - 404
canvas_not_found: the canvas is archived, or not in your organisation. - 503: nothing was written. Retry the same call.
Error messages never contain an address.