Skip to content

Commit fe05393

Browse files
SDK 12.2.0: triggers.ensureBrewEvent for Brew-event triggers (brew-v2#2071) (#66)
* SDK 12.4.0: triggers.ensureBrewEvent for Brew-event triggers (brew-v2#2071) GetBrew/brew-v2#2071 adds triggers Brew starts itself, the first being a contact unsubscribing. This mirrors that API: - `brew.automations.triggers.ensureBrewEvent({ event, scope, domainId? })` wraps `POST /v1/automations/triggers/brew-events`, the get-or-create for one scope, and returns the row plus `created`. - Exports `EnsureBrewEventTriggerInput`, `EnsureBrewEventTriggerResponse` and `BrewEventTriggerConfig`. - The bundled spec takes exactly brew-v2#2071's spec change; the consent-source sync stays with #62. Generated types are regenerated. Trigger `provider` and instance `source` gain `brew_event`, and the error codes gain `TRIGGER_NOT_FIREABLE`. - `docs/automations.md` gets an "Unsubscribe triggers" section, and the `fire` JSDoc names the 409. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Release as 12.2.0; spec delta for Brew-event test runs - 12.2.0, the next free version: this lands before the open 12.2.0 and 12.3.0 branches, which renumber when they merge. - Vendored spec delta from brew-v2#2071: the test operation says a Brew-event automation's test runs on Brew's sample with passed fields laid over it, and unsubscribe.reasons is required. Types regenerated. - Docs and changelog: the sample overlay, and that mail from another marketing domain reaches the contact only while they are still on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2f9d372 commit fe05393

12 files changed

Lines changed: 69471 additions & 82 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# Changelog
22

3+
## 12.2.0
4+
5+
Needs the API from GetBrew/brew-v2#2071: triggers Brew starts itself.
6+
7+
### Added
8+
9+
- `brew.automations.triggers.ensureBrewEvent({ event, scope, domainId? })`
10+
(`POST /v1/automations/triggers/brew-events`) gets or creates the brand's
11+
trigger for one Brew-event scope and says which with `created`. The first
12+
Brew event is `contact.unsubscribed`, a contact unsubscribing with an
13+
email's link or their mail app's one-click button. Use `scope: 'domain'`
14+
with a marketing `domainId` from `brew.domains.list()`, or `scope: 'all'`
15+
for every email the brand sends. New types: `EnsureBrewEventTriggerInput`,
16+
`EnsureBrewEventTriggerResponse` and `BrewEventTriggerConfig`.
17+
- Trigger rows can carry `provider: 'brew_event'` and a `brewEvent` config,
18+
and trigger instances `source: 'brew_event'`. A `switch` over either union
19+
that covers every value needs the new case.
20+
- The error code `TRIGGER_NOT_FIREABLE`. On a Brew-event trigger,
21+
`triggers.fire` and `triggers.readiness` answer `409 TRIGGER_NOT_FIREABLE`,
22+
and `triggers.patch` and `triggers.putContract` answer
23+
`422 TRIGGER_IMMUTABLE`. Try its automation with `automations.test`: the
24+
test runs on the trigger's sample, with any `payload` fields you pass laid
25+
over it, and writes no contact.
26+
327
## 12.0.0
428

529
The API retired `POST /v1/data` (GetBrew/brew-v2#1825), so the SDK drops the

‎docs/automations.md‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ scope.
2222
| [`triggerInstances.list` / `get`](#fired-trigger-history) | `/v1/automations/trigger-instances[/{triggerInstanceId}]` |
2323
| `triggers.list` / `create` / `patch` / `delete` | `/v1/automations/triggers[/{triggerEventId}]` |
2424
| `triggers.get` | `GET /v1/automations/triggers/{triggerEventId}` |
25+
| [`triggers.ensureBrewEvent`](#unsubscribe-triggers) | `POST /v1/automations/triggers/brew-events` |
2526
| [`triggers.fire`](#transactional-email--firing-triggers) | `POST /v1/automations/triggers/{triggerEventId}/fire` |
2627
| `triggers.readiness` | `GET /v1/automations/triggers/{triggerEventId}/readiness` |
2728
| `triggers.getContract` / `putContract` / `validatePayload` | `/v1/automations/triggers/{triggerEventId}/contract[…]` |
@@ -168,6 +169,54 @@ up.
168169
body. It replaced `triggers.ready`, which overloaded `GET …/fire` and wrapped
169170
its answer in the fire envelope.
170171

172+
## Unsubscribe triggers
173+
174+
Brew events are triggers Brew starts itself. The first one is
175+
`contact.unsubscribed`: a contact unsubscribed with an email's unsubscribe
176+
link or their mail app's one-click button (never a bounce, complaint, import
177+
or API edit). `triggers.ensureBrewEvent` returns the brand's trigger for one
178+
scope, creating it the first time:
179+
180+
```ts
181+
// Unsubscribes from one marketing domain's list (ids from brew.domains.list())
182+
const news = await brew.automations.triggers.ensureBrewEvent({
183+
event: 'contact.unsubscribed',
184+
scope: 'domain',
185+
domainId: 'jd7f0c2a8w1x9m4n6p3q5r8s2t',
186+
})
187+
188+
// Unsubscribes from every email the brand sends
189+
const all = await brew.automations.triggers.ensureBrewEvent({
190+
event: 'contact.unsubscribed',
191+
scope: 'all',
192+
})
193+
194+
all.created // true the first time, false on every later call
195+
all.brewEvent // { event: 'contact.unsubscribed', scope: 'all' }
196+
```
197+
198+
An unsubscribe from all clicked in one of a domain's emails also starts that
199+
domain's trigger, if the contact was still on its list. The payload is
200+
`{ email, unsubscribe: { scope, domain?, method, reasons, reasonText?, emailId?, emailTitle?, sendId?, at } }`.
201+
202+
Wire the `triggerEventId` into `automations.create`. Brew owns the row:
203+
`patch` and `putContract` answer `422 TRIGGER_IMMUTABLE`, and `fire` and
204+
`readiness` answer `409 TRIGGER_NOT_FIREABLE`. Try a flow with
205+
`automations.test({ automationId })`: the test runs on the trigger's sample,
206+
with any `payload` fields you pass laid over it, and writes no contact.
207+
208+
The run emails only the contact who just opted out. Send what they need, such
209+
as a confirmation of the unsubscribe, from a transactional domain; never a
210+
promotion. Mail from another marketing domain reaches them only while they are
211+
still on its list, never after an unsubscribe from all. Publishing refuses a
212+
flow none of whose emails could reach them.
213+
214+
Refusals: `400 INVALID_REQUEST` when `domainId` is missing for
215+
`scope: 'domain'` or sent for `scope: 'all'`; `404 DOMAIN_NOT_FOUND`;
216+
`422 DOMAIN_PURPOSE_NOT_ALLOWED` for a transactional domain, which keeps no
217+
list; `409 TRIGGER_LIMIT_REACHED` when the brand holds the most triggers it
218+
can.
219+
171220
## Fired-trigger history
172221

173222
`triggerInstances` is the audit log of every inbound fire — one row per fire,
@@ -193,7 +242,8 @@ for await (const row of brew.automations.triggerInstances.listAll()) {
193242
```
194243

195244
An unknown id is `404 TRIGGER_INSTANCE_NOT_FOUND` — the code that replaced
196-
`EVENT_NOT_FOUND`.
245+
`EVENT_NOT_FOUND`. `source` says where the fire came from: `api`,
246+
`integration`, or `brew_event` (an unsubscribe Brew recorded itself).
197247

198248
## Manual-audience runs
199249

0 commit comments

Comments
 (0)