Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -897,3 +897,11 @@ Recipient reads expose `failureReason` and accept `reason: "frequency_cap"`.
Analytics adds `frequencyCap.skippedRecipientCount`. Raising limits never retries
settled skips. `contracts/campaign-frequency-cap.json` pins the merged API source;
package publication and API availability remain separate release gates.

## Unreleased: audience segments and drip sequences

- Add organization-only audience segment CRUD, observed revisions and count previews.
- Add segment campaign selection and frozen snapshot reads.
- Add partial drip settings, enrollment, pause/enrollment reads and step analytics types.
- Bind focused contracts to merged API `f46ebd5162453e812dafa76ce02b420ddabe6921`.
Package publication and matching environment acceptance remain pending.
5 changes: 5 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,3 +632,8 @@ Recipient reads expose `failureReason` and accept `reason: "frequency_cap"`.
Analytics adds `frequencyCap.skippedRecipientCount`. Raising limits never retries
settled skips. `contracts/campaign-frequency-cap.json` pins the merged API source;
package publication and API availability remain separate release gates.

`campaign-segments-drip.json` pins the merged audience segment and drip additions
at API dev `f46ebd5162453e812dafa76ce02b420ddabe6921`, including source hashes,
focused schemas, fields and operations. The full baseline snapshots keep their
independent pins. This file records source adoption, not release availability.
2,708 changes: 2,708 additions & 0 deletions contracts/campaign-segments-drip.json

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions packages/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3181,3 +3181,37 @@ Recipient reads expose `failureReason` and accept `reason: "frequency_cap"`.
Analytics adds `frequencyCap.skippedRecipientCount`. Raising limits never retries
settled skips. `contracts/campaign-frequency-cap.json` pins the merged API source;
package publication and API availability remain separate release gates.

### Audience segments and drip sequences (source support, publication pending)

`platform.audienceSegments` supports `list`, `retrieve`, `create`, `update`,
`delete`, `preview` (saved rules) and `previewRules` (unsaved rules). Updates
and deletion require the observed `expectedRevision`. Team API keys require
`campaigns:read` for reads and saved previews, or `campaigns:manage` for writes
and unsaved previews. Project tokens cannot manage segments. Both audience
betas must be available for create, update and previews (`403 feature_unavailable`).
Missing authority returns `403 permission_denied`. Choose a segment with
`platform.campaigns.create({ projectId, name, segmentId })`; launch freezes
`segmentSnapshot` (revision, evaluation time, matched count). A deleted segment
cannot relaunch an already launched campaign (`409 state_conflict`). A bounded
segment evaluation timeout also returns `409 state_conflict`: simplify the
rules and retry. Revision conflicts require a fresh read and reconciliation.
No mutation is automatically retried by the segment resource.

Both campaign APIs accept nullable `drip` create settings and a partial drip
PATCH. Omitted PATCH fields preserve stored values. To close continuous enrollment,
patch only `{ drip: { enrollment: "audience" } }`; unchanged exit flags are
preserved, including after beta withdrawal. `enrollmentOpen` and `pauseReason`
are nullable read fields; enrollment additionally requires running/paused state.
`campaigns.enroll` posts one phone and optional variables; a previously enrolled
phone returns `created: false`, including contacts who finished or exited.
Closed or non-drip enrollment returns `409 state_conflict`.

Analytics exposes `drip.steps` counters. Recipient `sequenceError` includes
`replied`, `link_clicked`, `opted_out` and open-ended send failure codes.
Later messages accept a step `sendWindow` with one to seven weekdays and one
to four hour ranges. `delayAfterSec` is at most 2,592,000 for drips (86,400 for
ordinary sequences) and must be a whole multiple of `delayUnit` when supplied.
Tracked-link GETs, including scanner/preview requests, trigger enabled exits;
HEAD requests do not. Server beta, sender, consent, opt-out and funding checks
remain authoritative. These additions are not proof of registry availability.
3 changes: 3 additions & 0 deletions packages/typescript/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "./platform/call-consent.js";
import { PlatformCallsResource } from "./platform/calls.js";
import { CallRetentionResource } from "./platform/call-retention.js";
import { AudienceSegmentsResource } from "./platform/audience-segments.js";
import { CampaignsResource } from "./platform/campaigns.js";
import { CustomersResource } from "./platform/customers.js";
import {
Expand Down Expand Up @@ -93,6 +94,7 @@ export interface OrganizationControlPlaneResources {
/** The team's do-not-call list. */
readonly callOptOuts: CallOptOutsResource;
readonly campaigns: CampaignsResource;
readonly audienceSegments: AudienceSegmentsResource;
readonly customers: CustomersResource;
readonly members: MembersResource;
readonly organizations: OrganizationsResource;
Expand Down Expand Up @@ -226,6 +228,7 @@ class ClientImplementation implements ClientBase<ClientOwner> {
callPolicy: new CallPolicyResource(this.#transport),
callOptOuts: new CallOptOutsResource(this.#transport),
campaigns: new CampaignsResource(this.#transport),
audienceSegments: new AudienceSegmentsResource(this.#transport),
customers: new CustomersResource(this.#transport),
members: new MembersResource(this.#transport),
organizations: new OrganizationsResource(this.#transport),
Expand Down
3 changes: 3 additions & 0 deletions packages/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1334,3 +1334,6 @@ export type {
CampaignFrequencyCapSettings,
UpdateCampaignFrequencyCapInput,
} from "./platform/types.js";

export * from "./platform/audience-segments.js";
export * from "./messaging/campaign-drip.js";
69 changes: 69 additions & 0 deletions packages/typescript/src/messaging/campaign-drip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type {
CampaignSendWindowDay,
CampaignSendWindowRange,
CampaignRecipientVariables,
} from "./types.js";
export interface CampaignDrip {
readonly enrollment: "audience" | "continuous";
readonly exitOnReply: boolean;
/** GET opens, including scanners/previews, count; HEAD does not. */
readonly exitOnLinkClick: boolean;
}
/** Create defaults: audience, true, false. Beta, linked-device/test senders only. */
export type CampaignDripInput = Partial<CampaignDrip>;
/** Omitted settings preserve stored values. No defaults are applied to PATCH.
* Closing with only enrollment: audience also works after beta withdrawal. */
export type CampaignDripPatch = Partial<CampaignDrip>;
export interface CampaignDripStepStats {
readonly step: number;
readonly entered: number;
readonly waiting: number;
readonly sent: number;
readonly delivered: number;
readonly read: number;
readonly replied: number;
readonly exited: number;
readonly failed: number;
}
export interface CampaignDripStats {
readonly enrollment: "audience" | "continuous";
readonly enrolled: number;
readonly enrolledByApi: number;
readonly active: number;
readonly completed: number;
readonly exited: number;
readonly steps: readonly CampaignDripStepStats[];
}
export interface EnrollCampaignContactRequest {
readonly phone: string;
readonly variables?: CampaignRecipientVariables;
}
export interface CampaignEnrollment {
readonly campaignId: string;
readonly recipientId: string;
readonly phone: string;
readonly created: boolean;
readonly status: string;
readonly enrolledAt: number;
}
export interface CampaignSegmentSnapshot {
readonly revision: number;
readonly evaluatedAt: number;
readonly matchedCount: number;
}
/** Later steps only. At most 4 hour ranges, same time zone as the campaign.
* Delay is 0..2592000 seconds, in whole multiples of delayUnit. */
export interface CampaignStepWindow {
readonly days: readonly CampaignSendWindowDay[];
readonly hours: readonly CampaignSendWindowRange[];
}
export interface CampaignSequenceMessage {
readonly version?: 2;
readonly source?: string;
readonly delayAfterSec?: number;
readonly delayUnit?: "minutes" | "hours" | "days";
readonly sendWindow?: CampaignStepWindow | null;
readonly [field: string]: unknown;
}
/** Exposed through recipient.sequenceError; arbitrary send failure codes also occur. */
export type CampaignDripExitReason = "replied" | "link_clicked" | "opted_out";
Comment thread
purpshell marked this conversation as resolved.
22 changes: 22 additions & 0 deletions packages/typescript/src/messaging/campaigns.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type {
CampaignEnrollment,
EnrollCampaignContactRequest,
} from "./campaign-drip.js";
import { HttpTransport } from "../transport/http.js";
import {
withIdempotencyKey,
Expand Down Expand Up @@ -29,6 +33,24 @@ import type {
export class MessagingCampaignsResource {
constructor(private readonly transport: HttpTransport) {}

/** Continuous drip enrollment; existing phones return created:false and never restart. */
enroll(
projectSlug: string,
campaignId: string,
body: EnrollCampaignContactRequest,
options: RequestOptions = {},
): Promise<
ApiResponse<{ readonly success: true; readonly data: CampaignEnrollment }>
> {
return this.transport.request({
method: "POST",
path: `${campaignPath(projectSlug, campaignId)}/enrollments`,
body,
...options,
maxNetworkRetries: 0,
});
}

list(
projectSlug: string,
options: RequestOptions = {},
Expand Down
21 changes: 21 additions & 0 deletions packages/typescript/src/messaging/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import type {
CampaignDrip,
CampaignDripInput,
CampaignDripPatch,
CampaignDripStats,
CampaignSegmentSnapshot,
} from "./campaign-drip.js";
/** Exact observed provider references; at least one provider is known. */
export type WhatsAppMessageIds =
| { readonly linked_devices: string; readonly official_api?: string }
Expand Down Expand Up @@ -280,6 +287,11 @@ export interface CampaignFollowUpReport {

/** Project-scoped campaign summary returned by the Messaging API. */
export interface Campaign {
readonly drip?: CampaignDrip | null;
readonly enrollmentOpen?: boolean | null;
readonly pauseReason?: string | null;
readonly segmentId?: string | null;
readonly segmentSnapshot?: CampaignSegmentSnapshot | null;
readonly id: string;
readonly name: string;
/** The pinned contract deliberately leaves campaign states forward-compatible. */
Expand Down Expand Up @@ -391,6 +403,7 @@ export type CampaignRecipientFailureReason =
| "other";

export interface CampaignAnalytics {
readonly drip?: CampaignDripStats | null;
readonly frequencyCap?: { readonly skippedRecipientCount: number };
readonly campaignId: string;
readonly recipientCount: number;
Expand All @@ -408,6 +421,7 @@ export interface CampaignAnalytics {
}

export interface CreateCampaignRequest {
readonly drip?: CampaignDripInput | null;
readonly name: string;
readonly templateId?: string;
readonly recipientListId?: string;
Expand Down Expand Up @@ -464,6 +478,11 @@ export interface CampaignRecipientInput {

/** One queued or settled recipient of a campaign. */
export interface CampaignRecipient {
/** Number of accepted sequence messages; delivery timestamps below describe step 0. */
readonly messagesSent?: number;
readonly nextMessageAt?: number | null;
/** Includes replied, link_clicked, opted_out and open-ended send failures. */
readonly sequenceError?: string | null;
readonly id: string;
readonly phone: string;
readonly variables: Readonly<Record<string, unknown>>;
Expand Down Expand Up @@ -528,6 +547,7 @@ export interface LaunchCampaignRequest {
}

interface UpdateCampaignChanges {
readonly drip?: CampaignDripPatch | null;
readonly name?: string;
readonly recipientListId?: string | null;
readonly senderConfig?: Readonly<Record<string, unknown>>;
Expand All @@ -551,6 +571,7 @@ interface UpdateCampaignChanges {

/** At least one draft field must be supplied. */
export type UpdateCampaignRequest =
| (UpdateCampaignChanges & { readonly drip: CampaignDripPatch | null })
| (UpdateCampaignChanges & {
readonly frequencyCap: CampaignFrequencyCap | null;
})
Expand Down
Loading
Loading