@posthaste/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +454 -0
- package/dist/client.d.ts +234 -0
- package/dist/client.js +459 -0
- package/dist/errors.d.ts +100 -0
- package/dist/errors.js +154 -0
- package/dist/http.d.ts +150 -0
- package/dist/http.js +258 -0
- package/dist/ids.d.ts +25 -0
- package/dist/ids.js +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/pagination.d.ts +45 -0
- package/dist/pagination.js +69 -0
- package/dist/types.d.ts +593 -0
- package/dist/types.js +76 -0
- package/dist/webhooks.d.ts +70 -0
- package/dist/webhooks.js +130 -0
- package/package.json +44 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyset pagination.
|
|
3
|
+
*
|
|
4
|
+
* The API pages by cursor rather than by offset: `limit` + `before`, answering
|
|
5
|
+
* with `{ data, hasMore, nextCursor }`. `before` is the id of the last row you
|
|
6
|
+
* received, which is what `nextCursor` hands you.
|
|
7
|
+
*
|
|
8
|
+
* THE TRAP THIS MODULE EXISTS TO CLOSE
|
|
9
|
+
*
|
|
10
|
+
* The obvious loop is `while (nextCursor) { … }`. It is wrong. `nextCursor` is
|
|
11
|
+
* derived from the last row of a page, and at least one endpoint on this API
|
|
12
|
+
* returns a non-null cursor on its final page — the documentation calls it out
|
|
13
|
+
* for `/v1/inbound/messages`. A cursor loop over such an endpoint asks for the
|
|
14
|
+
* page after the last one, gets an empty page with the same cursor back, and
|
|
15
|
+
* spins forever. Nothing about it looks wrong in a log; it just never finishes.
|
|
16
|
+
*
|
|
17
|
+
* `hasMore` is the server's actual answer to "is there another page", computed
|
|
18
|
+
* by fetching one row more than you asked for. It is the only correct
|
|
19
|
+
* condition, so `autoPaginate` is the API this SDK puts in front of people —
|
|
20
|
+
* `list` is still there for anyone who wants a single page.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Walk every page and yield every item.
|
|
24
|
+
*
|
|
25
|
+
* Belt and braces, because an infinite loop in someone's job runner is a bad
|
|
26
|
+
* way to learn about a contract change:
|
|
27
|
+
*
|
|
28
|
+
* - stops on `hasMore === false`, always;
|
|
29
|
+
* - stops if a page comes back empty, because there is nothing to advance to;
|
|
30
|
+
* - stops if the cursor does not MOVE, which is what a non-null terminal
|
|
31
|
+
* cursor looks like from here.
|
|
32
|
+
*/
|
|
33
|
+
export async function* autoPaginate(fetchPage, params) {
|
|
34
|
+
let before = params.before;
|
|
35
|
+
let seen;
|
|
36
|
+
for (;;) {
|
|
37
|
+
const page = await fetchPage({ ...params, before });
|
|
38
|
+
for (const item of page.data)
|
|
39
|
+
yield item;
|
|
40
|
+
if (!page.hasMore)
|
|
41
|
+
return;
|
|
42
|
+
if (page.data.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
// Prefer the server's cursor; fall back to the last row's id, which is what
|
|
45
|
+
// the cursor is anyway. Either way the NEXT request must ask for something
|
|
46
|
+
// different from the last one.
|
|
47
|
+
const next = page.nextCursor ?? page.data[page.data.length - 1]?.id;
|
|
48
|
+
if (!next || next === seen)
|
|
49
|
+
return;
|
|
50
|
+
seen = next;
|
|
51
|
+
before = next;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Collect an auto-paginated stream into an array.
|
|
56
|
+
*
|
|
57
|
+
* `maxItems` is not optional garnish. Draining an unbounded list into memory is
|
|
58
|
+
* how a helper like this turns into an incident on the one account that has
|
|
59
|
+
* four million messages, so there is a ceiling and the caller chooses it.
|
|
60
|
+
*/
|
|
61
|
+
export async function collect(source, maxItems) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for await (const item of source) {
|
|
64
|
+
out.push(item);
|
|
65
|
+
if (out.length >= maxItems)
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire types.
|
|
3
|
+
*
|
|
4
|
+
* Every shape here was written from the handler that produces it — see
|
|
5
|
+
* `apps/api/src/app.ts`, `billing-routes.ts`, `dashboard-routes.ts` and
|
|
6
|
+
* `cloudflare-routes.ts` — rather than from the documentation, so a field
|
|
7
|
+
* present here is a field the server actually sends.
|
|
8
|
+
*
|
|
9
|
+
* Timestamps are ISO-8601 strings. They are `timestamptz` in Postgres and are
|
|
10
|
+
* JSON-serialised on the way out, so they arrive as strings however they were
|
|
11
|
+
* stored; typing them as `Date` would be a lie that only shows up at runtime.
|
|
12
|
+
*/
|
|
13
|
+
import type { AccountId, ApiKeyId, DomainId, InvoiceId, MessageId, PaymentId, SubscriptionId, SuppressionId, WebhookId } from './ids.js';
|
|
14
|
+
/** `message_status` in the database. The lifecycle a message can be in. */
|
|
15
|
+
export declare const MESSAGE_STATUSES: readonly ['queued', 'sending', 'delivered', 'bounced', 'complained', 'failed', 'rejected'];
|
|
16
|
+
export type MessageStatus = (typeof MESSAGE_STATUSES)[number];
|
|
17
|
+
/**
|
|
18
|
+
* The ten webhook event types (`event_type` in the database).
|
|
19
|
+
*
|
|
20
|
+
* Note that these are NOT the same set as `MessageStatus`: `accepted`,
|
|
21
|
+
* `attempted`, `deferred` and `suppressed` are things that happen to a message
|
|
22
|
+
* without changing the status it rests in.
|
|
23
|
+
*/
|
|
24
|
+
export declare const EVENT_TYPES: readonly ['accepted', 'queued', 'attempted', 'delivered', 'deferred', 'bounced', 'complained', 'failed', 'rejected', 'suppressed'];
|
|
25
|
+
export type EventType = (typeof EVENT_TYPES)[number];
|
|
26
|
+
/**
|
|
27
|
+
* `suppression_reason` in the database.
|
|
28
|
+
*
|
|
29
|
+
* Spelled exactly as the server spells it. `hard_bounce` is by far the most
|
|
30
|
+
* common one, and guessing at `bounce` produces a filter that silently matches
|
|
31
|
+
* nothing rather than an error.
|
|
32
|
+
*/
|
|
33
|
+
export declare const SUPPRESSION_REASONS: readonly ['hard_bounce', 'complaint', 'manual', 'unsubscribe', 'spam_trap'];
|
|
34
|
+
export type SuppressionReason = (typeof SUPPRESSION_REASONS)[number];
|
|
35
|
+
/** `domain_status`. */
|
|
36
|
+
export type DomainStatus = 'pending' | 'verified' | 'failed' | 'disabled';
|
|
37
|
+
/** `account_status`. */
|
|
38
|
+
export type AccountStatus = 'active' | 'suspended' | 'closed';
|
|
39
|
+
/** `webhook_status`. */
|
|
40
|
+
export type WebhookStatus = 'active' | 'disabled';
|
|
41
|
+
/** `subscription_status`. */
|
|
42
|
+
export type SubscriptionStatus = 'incomplete' | 'active' | 'past_due' | 'paused' | 'cancelled' | 'expired';
|
|
43
|
+
/** `payment_status`. */
|
|
44
|
+
export type PaymentStatus = 'created' | 'authorized' | 'captured' | 'failed' | 'refunded' | 'partially_refunded';
|
|
45
|
+
export type PlanId = 'free' | 'starter' | 'growth' | 'scale' | 'enterprise';
|
|
46
|
+
export type KeyEnvironment = 'live' | 'test';
|
|
47
|
+
/**
|
|
48
|
+
* Every scope an API key can be granted.
|
|
49
|
+
*
|
|
50
|
+
* `billing:read` is absent on purpose: it is not in the API's grantable set —
|
|
51
|
+
* a session carries it, and the billing reads also accept `account:read`,
|
|
52
|
+
* which is what an API key uses to reach them.
|
|
53
|
+
*/
|
|
54
|
+
export declare const SCOPES: readonly ['account:read', 'domains:read', 'domains:write', 'emails:send', 'messages:read', 'suppressions:read', 'suppressions:write', 'webhooks:read', 'webhooks:write'];
|
|
55
|
+
export type Scope = (typeof SCOPES)[number];
|
|
56
|
+
/**
|
|
57
|
+
* A keyset page.
|
|
58
|
+
*
|
|
59
|
+
* `hasMore` is the ONLY correct loop condition. `nextCursor` is populated from
|
|
60
|
+
* the last row of a full page, and there are endpoints and edge cases where it
|
|
61
|
+
* is non-null on the final page — a `while (nextCursor)` loop over those never
|
|
62
|
+
* terminates. `autoPaginate` on each resource encodes the right condition so
|
|
63
|
+
* this cannot be got wrong by hand.
|
|
64
|
+
*/
|
|
65
|
+
export interface Page<T> {
|
|
66
|
+
data: T[];
|
|
67
|
+
hasMore: boolean;
|
|
68
|
+
nextCursor: string | null;
|
|
69
|
+
}
|
|
70
|
+
/** A list that is not paginated. Returned whole. */
|
|
71
|
+
export interface List<T> {
|
|
72
|
+
data: T[];
|
|
73
|
+
}
|
|
74
|
+
export interface PaginationParams {
|
|
75
|
+
/** Page size. Messages cap at 100, suppressions at 200. */
|
|
76
|
+
limit?: number;
|
|
77
|
+
/** The `nextCursor` from the previous page. */
|
|
78
|
+
before?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface AccountPlan {
|
|
81
|
+
id: PlanId;
|
|
82
|
+
name: string;
|
|
83
|
+
monthlyEmails: number;
|
|
84
|
+
/** `null` means unlimited. */
|
|
85
|
+
maxDomains: number | null;
|
|
86
|
+
retentionDays: number;
|
|
87
|
+
dedicatedIp: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface AccountSubscriptionSummary {
|
|
90
|
+
status: SubscriptionStatus;
|
|
91
|
+
graceEndsAt: string | null;
|
|
92
|
+
cancelAtPeriodEnd: boolean;
|
|
93
|
+
}
|
|
94
|
+
export interface AccountSending {
|
|
95
|
+
dailyLimit: number;
|
|
96
|
+
sentToday: number;
|
|
97
|
+
remainingToday: number;
|
|
98
|
+
warmupTier: number;
|
|
99
|
+
sentThisMonth: number;
|
|
100
|
+
remainingThisMonth: number;
|
|
101
|
+
}
|
|
102
|
+
export interface Account {
|
|
103
|
+
id: AccountId;
|
|
104
|
+
name: string;
|
|
105
|
+
slug: string;
|
|
106
|
+
status: AccountStatus;
|
|
107
|
+
environment: KeyEnvironment;
|
|
108
|
+
scopes: string[];
|
|
109
|
+
/** Null only on an account with no warmup row — in practice, never. */
|
|
110
|
+
plan: AccountPlan | null;
|
|
111
|
+
/** Present only while there is something to say about the subscription. */
|
|
112
|
+
subscription: AccountSubscriptionSummary | null;
|
|
113
|
+
sending: AccountSending | null;
|
|
114
|
+
}
|
|
115
|
+
/** `GET /v1/account/verify` — a replay of the whole event chain. */
|
|
116
|
+
export interface ChainVerification {
|
|
117
|
+
intact: boolean;
|
|
118
|
+
/** The sequence number at which the record stops being trustworthy. */
|
|
119
|
+
brokenAt: number | null;
|
|
120
|
+
problem: string | null;
|
|
121
|
+
checked: string;
|
|
122
|
+
}
|
|
123
|
+
export interface UsageDay {
|
|
124
|
+
/** `YYYY-MM-DD`. */
|
|
125
|
+
day: string;
|
|
126
|
+
sent: number;
|
|
127
|
+
delivered: number;
|
|
128
|
+
bounced: number;
|
|
129
|
+
complained: number;
|
|
130
|
+
}
|
|
131
|
+
export interface Usage {
|
|
132
|
+
/** `YYYY-MM-DD`, first day of the current UTC month. */
|
|
133
|
+
periodStart: string;
|
|
134
|
+
periodEnd: string;
|
|
135
|
+
sent: number;
|
|
136
|
+
delivered: number;
|
|
137
|
+
bounced: number;
|
|
138
|
+
complained: number;
|
|
139
|
+
/** Null while nothing has been sent — not zero, which would be a claim. */
|
|
140
|
+
deliveryRate: number | null;
|
|
141
|
+
complaintRate: number | null;
|
|
142
|
+
bounceRate: number | null;
|
|
143
|
+
days: UsageDay[];
|
|
144
|
+
}
|
|
145
|
+
export interface DnsRecord {
|
|
146
|
+
name: string;
|
|
147
|
+
type: 'TXT' | 'MX' | 'CNAME';
|
|
148
|
+
value: string;
|
|
149
|
+
/** TXT values over 255 characters have to be published as split strings. */
|
|
150
|
+
chunks?: string[];
|
|
151
|
+
/** Only required records gate verification. Only DKIM is required. */
|
|
152
|
+
required: boolean;
|
|
153
|
+
purpose: string;
|
|
154
|
+
/** Present when publishing the record carries a risk (SPF, DMARC). */
|
|
155
|
+
warning?: string;
|
|
156
|
+
}
|
|
157
|
+
export interface CreateDomainParams {
|
|
158
|
+
name: string;
|
|
159
|
+
}
|
|
160
|
+
export interface CreatedDomain {
|
|
161
|
+
id: DomainId;
|
|
162
|
+
name: string;
|
|
163
|
+
status: 'pending';
|
|
164
|
+
records: DnsRecord[];
|
|
165
|
+
}
|
|
166
|
+
export interface Domain {
|
|
167
|
+
id: DomainId;
|
|
168
|
+
name: string;
|
|
169
|
+
status: DomainStatus;
|
|
170
|
+
verifiedAt: string | null;
|
|
171
|
+
createdAt: string;
|
|
172
|
+
/** Which DKIM key signs this domain's mail, so a rotation is visible. */
|
|
173
|
+
selector: string;
|
|
174
|
+
/** Messages ever sent from it. Non-zero is what makes deletion refused. */
|
|
175
|
+
messagesSent: number;
|
|
176
|
+
lastCheckedAt: string | null;
|
|
177
|
+
/** Returned on the LIST too, so the setup values are never lost with a tab. */
|
|
178
|
+
records: DnsRecord[];
|
|
179
|
+
}
|
|
180
|
+
export type CheckStatus = 'pass' | 'fail' | 'not_found';
|
|
181
|
+
export interface RecordCheck {
|
|
182
|
+
record: 'dkim' | 'spf' | 'dmarc';
|
|
183
|
+
name: string;
|
|
184
|
+
required: boolean;
|
|
185
|
+
status: CheckStatus;
|
|
186
|
+
detail: string;
|
|
187
|
+
}
|
|
188
|
+
export interface DomainVerification {
|
|
189
|
+
id: DomainId;
|
|
190
|
+
name: string;
|
|
191
|
+
status: 'verified' | 'failed';
|
|
192
|
+
verified: boolean;
|
|
193
|
+
/** Every check is reported, passing or not. */
|
|
194
|
+
checks: RecordCheck[];
|
|
195
|
+
}
|
|
196
|
+
export interface DnsHost {
|
|
197
|
+
id: string;
|
|
198
|
+
name: string;
|
|
199
|
+
url: string;
|
|
200
|
+
note: string | null;
|
|
201
|
+
nameservers: string[];
|
|
202
|
+
}
|
|
203
|
+
export interface DomainSetup {
|
|
204
|
+
/** Who runs this domain's DNS, when we could work it out. */
|
|
205
|
+
host: DnsHost | null;
|
|
206
|
+
/** Present only when the provider supports one-click publishing. */
|
|
207
|
+
oneClick: {
|
|
208
|
+
provider: string;
|
|
209
|
+
url: string;
|
|
210
|
+
} | null;
|
|
211
|
+
}
|
|
212
|
+
export interface ConnectCloudflareParams {
|
|
213
|
+
/**
|
|
214
|
+
* A Cloudflare API token with Zone:DNS:Edit on the zone. Optional only when
|
|
215
|
+
* a token was previously stored with `remember`.
|
|
216
|
+
*/
|
|
217
|
+
token?: string;
|
|
218
|
+
/** Store the token, so adding a second domain needs no paste. */
|
|
219
|
+
remember?: boolean;
|
|
220
|
+
}
|
|
221
|
+
export interface CloudflarePublishResult {
|
|
222
|
+
published: Array<{
|
|
223
|
+
type: 'TXT';
|
|
224
|
+
name: string;
|
|
225
|
+
created: boolean;
|
|
226
|
+
}>;
|
|
227
|
+
zone: string;
|
|
228
|
+
verified: boolean;
|
|
229
|
+
message: string;
|
|
230
|
+
/** SPF and DMARC — deliberately left alone, and said so. */
|
|
231
|
+
notPublished: Array<{
|
|
232
|
+
type: string;
|
|
233
|
+
name: string;
|
|
234
|
+
value: string;
|
|
235
|
+
why: string;
|
|
236
|
+
}>;
|
|
237
|
+
}
|
|
238
|
+
export interface SendEmailParams {
|
|
239
|
+
from: string;
|
|
240
|
+
to: string;
|
|
241
|
+
subject?: string;
|
|
242
|
+
text?: string;
|
|
243
|
+
html?: string;
|
|
244
|
+
replyTo?: string;
|
|
245
|
+
headers?: Record<string, string>;
|
|
246
|
+
listUnsubscribe?: string;
|
|
247
|
+
/**
|
|
248
|
+
* Idempotency key — a BODY field, not a header.
|
|
249
|
+
*
|
|
250
|
+
* The `Idempotency-Key` HTTP header is CORS-allowlisted by the API but no
|
|
251
|
+
* handler reads it, so sending it buys nothing. This field is the one the
|
|
252
|
+
* server checks. Supplying it also makes the send safe for this SDK to
|
|
253
|
+
* retry; without it a send is never retried automatically.
|
|
254
|
+
*/
|
|
255
|
+
idempotencyKey?: string;
|
|
256
|
+
}
|
|
257
|
+
export interface SendEmailResult {
|
|
258
|
+
id: MessageId;
|
|
259
|
+
/**
|
|
260
|
+
* `queued` for a new send (HTTP 202), `duplicate` for an idempotency replay
|
|
261
|
+
* (HTTP 200). Both are success, and both carry the id of the one real
|
|
262
|
+
* message.
|
|
263
|
+
*/
|
|
264
|
+
status: 'queued' | 'duplicate';
|
|
265
|
+
/**
|
|
266
|
+
* True when this request matched an earlier one by `idempotencyKey` and no
|
|
267
|
+
* new message was created. Surfaced rather than hidden, because "we did
|
|
268
|
+
* nothing, here is the thing you already sent" is a materially different
|
|
269
|
+
* answer from "accepted".
|
|
270
|
+
*/
|
|
271
|
+
duplicate: boolean;
|
|
272
|
+
}
|
|
273
|
+
export interface ListMessagesParams extends PaginationParams {
|
|
274
|
+
/** Max 100, default 25. */
|
|
275
|
+
limit?: number;
|
|
276
|
+
status?: MessageStatus;
|
|
277
|
+
/** Exact recipient. Resolves through an equality index. */
|
|
278
|
+
to?: string;
|
|
279
|
+
/** Free text over recipient and subject. */
|
|
280
|
+
search?: string;
|
|
281
|
+
/** A `dom_` id, not a domain name. */
|
|
282
|
+
domain?: string;
|
|
283
|
+
/** ISO-8601. Inclusive. */
|
|
284
|
+
from?: string;
|
|
285
|
+
/** ISO-8601. Exclusive. */
|
|
286
|
+
until?: string;
|
|
287
|
+
}
|
|
288
|
+
export interface MessageSummary {
|
|
289
|
+
id: MessageId;
|
|
290
|
+
from: string;
|
|
291
|
+
to: string;
|
|
292
|
+
subject: string | null;
|
|
293
|
+
status: MessageStatus;
|
|
294
|
+
attempts: number;
|
|
295
|
+
smtpCode: number | null;
|
|
296
|
+
createdAt: string;
|
|
297
|
+
}
|
|
298
|
+
export interface WaybillEntry {
|
|
299
|
+
seq: number;
|
|
300
|
+
type: EventType;
|
|
301
|
+
at: string;
|
|
302
|
+
detail: Record<string, unknown> | null;
|
|
303
|
+
hash: string;
|
|
304
|
+
prevHash: string;
|
|
305
|
+
}
|
|
306
|
+
export interface MessageContent {
|
|
307
|
+
text: string | null;
|
|
308
|
+
html: string | null;
|
|
309
|
+
/** The composed, signed message as it went on the wire. */
|
|
310
|
+
raw: string | null;
|
|
311
|
+
replyTo: string | null;
|
|
312
|
+
headers: Record<string, string>;
|
|
313
|
+
}
|
|
314
|
+
export interface Message {
|
|
315
|
+
id: MessageId;
|
|
316
|
+
/** The hash chain over this message's own events was re-verified on read. */
|
|
317
|
+
recordIntact: boolean;
|
|
318
|
+
/** Present only when `recordIntact` is false. */
|
|
319
|
+
recordProblem?: string;
|
|
320
|
+
from: string;
|
|
321
|
+
to: string;
|
|
322
|
+
subject: string | null;
|
|
323
|
+
status: MessageStatus;
|
|
324
|
+
attempts: number;
|
|
325
|
+
smtp: {
|
|
326
|
+
code: number;
|
|
327
|
+
message: string | null;
|
|
328
|
+
} | null;
|
|
329
|
+
createdAt: string;
|
|
330
|
+
updatedAt: string;
|
|
331
|
+
/** Null once the body has aged out of the plan's retention window. */
|
|
332
|
+
content: MessageContent | null;
|
|
333
|
+
/** Every hand the message passed through, with its hash linkage. */
|
|
334
|
+
waybill: WaybillEntry[];
|
|
335
|
+
}
|
|
336
|
+
export interface MessageStatsParams {
|
|
337
|
+
/** 1–365, default 30. */
|
|
338
|
+
days?: number;
|
|
339
|
+
/** A `dom_` id. */
|
|
340
|
+
domain?: string;
|
|
341
|
+
/** An IANA zone, so buckets land on your midnight rather than UTC's. */
|
|
342
|
+
tz?: string;
|
|
343
|
+
}
|
|
344
|
+
export interface StatsDay {
|
|
345
|
+
/** `YYYY-MM-DD`. */
|
|
346
|
+
day: string;
|
|
347
|
+
delivered: number;
|
|
348
|
+
bounced: number;
|
|
349
|
+
complained: number;
|
|
350
|
+
failed: number;
|
|
351
|
+
rejected: number;
|
|
352
|
+
pending: number;
|
|
353
|
+
total: number;
|
|
354
|
+
}
|
|
355
|
+
export interface StatsTotals {
|
|
356
|
+
delivered: number;
|
|
357
|
+
bounced: number;
|
|
358
|
+
complained: number;
|
|
359
|
+
failed: number;
|
|
360
|
+
rejected: number;
|
|
361
|
+
pending: number;
|
|
362
|
+
total: number;
|
|
363
|
+
/** Everything that has finished. Rates are over this, not over `total`. */
|
|
364
|
+
settled: number;
|
|
365
|
+
deliveryRate: number | null;
|
|
366
|
+
bounceRate: number | null;
|
|
367
|
+
complaintRate: number | null;
|
|
368
|
+
}
|
|
369
|
+
export interface MessageStats {
|
|
370
|
+
data: StatsDay[];
|
|
371
|
+
totals: StatsTotals;
|
|
372
|
+
/** The same window immediately before this one, for comparison. */
|
|
373
|
+
previous: StatsTotals;
|
|
374
|
+
change: {
|
|
375
|
+
total: number | null;
|
|
376
|
+
delivered: number | null;
|
|
377
|
+
bounced: number | null;
|
|
378
|
+
complained: number | null;
|
|
379
|
+
};
|
|
380
|
+
busiest: {
|
|
381
|
+
day: string;
|
|
382
|
+
total: number;
|
|
383
|
+
} | null;
|
|
384
|
+
/** The industry thresholds a sender is judged against, as percentages. */
|
|
385
|
+
thresholds: {
|
|
386
|
+
complaintRate: number;
|
|
387
|
+
bounceRate: number;
|
|
388
|
+
};
|
|
389
|
+
range: {
|
|
390
|
+
days: number;
|
|
391
|
+
tz: string;
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
export interface ListSuppressionsParams extends PaginationParams {
|
|
395
|
+
/** Max 200, default 50. */
|
|
396
|
+
limit?: number;
|
|
397
|
+
/** Partial address match. */
|
|
398
|
+
search?: string;
|
|
399
|
+
reason?: SuppressionReason;
|
|
400
|
+
}
|
|
401
|
+
export interface Suppression {
|
|
402
|
+
id: SuppressionId;
|
|
403
|
+
address: string;
|
|
404
|
+
reason: SuppressionReason;
|
|
405
|
+
detail: string | null;
|
|
406
|
+
createdAt: string;
|
|
407
|
+
/** A `platform` entry is not the account's to remove. */
|
|
408
|
+
scope: 'platform' | 'account';
|
|
409
|
+
}
|
|
410
|
+
export interface CreateSuppressionParams {
|
|
411
|
+
address: string;
|
|
412
|
+
/** Free text stored as the entry's detail. The reason is always `manual`. */
|
|
413
|
+
reason?: string;
|
|
414
|
+
}
|
|
415
|
+
export interface CreatedSuppression {
|
|
416
|
+
/** Normalised — lower-cased and trimmed by the server. */
|
|
417
|
+
address: string;
|
|
418
|
+
reason: 'manual';
|
|
419
|
+
}
|
|
420
|
+
export interface CreateWebhookParams {
|
|
421
|
+
/** http or https only. */
|
|
422
|
+
url: string;
|
|
423
|
+
/** Empty or absent means every event type. Max 20. */
|
|
424
|
+
eventTypes?: EventType[];
|
|
425
|
+
}
|
|
426
|
+
export interface CreatedWebhook {
|
|
427
|
+
id: WebhookId;
|
|
428
|
+
url: string;
|
|
429
|
+
eventTypes: EventType[];
|
|
430
|
+
status: 'active';
|
|
431
|
+
/**
|
|
432
|
+
* Shown exactly once, on creation, and never retrievable afterwards. Store
|
|
433
|
+
* it now — nobody, including us, can recover it.
|
|
434
|
+
*/
|
|
435
|
+
signingSecret: string;
|
|
436
|
+
}
|
|
437
|
+
export interface Webhook {
|
|
438
|
+
id: WebhookId;
|
|
439
|
+
url: string;
|
|
440
|
+
eventTypes: EventType[];
|
|
441
|
+
status: WebhookStatus;
|
|
442
|
+
createdAt: string;
|
|
443
|
+
}
|
|
444
|
+
/** The JSON body delivered to a webhook endpoint. */
|
|
445
|
+
export interface WebhookEvent {
|
|
446
|
+
/** Identical to the `posthaste-delivery-id` header. Deduplicate on it. */
|
|
447
|
+
id: string;
|
|
448
|
+
type: EventType;
|
|
449
|
+
createdAt: string;
|
|
450
|
+
data: {
|
|
451
|
+
/** Null for an event that is not about one specific message. */
|
|
452
|
+
messageId: MessageId | null;
|
|
453
|
+
detail?: Record<string, unknown>;
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
export interface ApiKey {
|
|
457
|
+
id: ApiKeyId;
|
|
458
|
+
name: string;
|
|
459
|
+
environment: KeyEnvironment;
|
|
460
|
+
scopes: string[];
|
|
461
|
+
/** Enough to recognise which key this is, not enough to use it. */
|
|
462
|
+
hint: string;
|
|
463
|
+
createdAt: string;
|
|
464
|
+
lastUsedAt: string | null;
|
|
465
|
+
revokedAt: string | null;
|
|
466
|
+
}
|
|
467
|
+
export interface BillingCharge {
|
|
468
|
+
currency: string;
|
|
469
|
+
totalMinor: number;
|
|
470
|
+
baseMinor: number;
|
|
471
|
+
taxMinor: number;
|
|
472
|
+
taxLabel: string | null;
|
|
473
|
+
exportOfServices: boolean;
|
|
474
|
+
display: string;
|
|
475
|
+
}
|
|
476
|
+
export interface BillingPlan {
|
|
477
|
+
id: PlanId;
|
|
478
|
+
name: string;
|
|
479
|
+
/** The one published price, identical for everyone. */
|
|
480
|
+
priceUsd: number;
|
|
481
|
+
/** Never inferred from `priceUsd === 0` — Free is zero, Enterprise is not. */
|
|
482
|
+
custom: boolean;
|
|
483
|
+
/** Only sent for custom plans. */
|
|
484
|
+
items?: string[];
|
|
485
|
+
monthlyEmails: number;
|
|
486
|
+
/** `null` means unlimited. */
|
|
487
|
+
maxDomains: number | null;
|
|
488
|
+
retentionDays: number;
|
|
489
|
+
/** What this customer would actually be charged, in their currency. */
|
|
490
|
+
charge: BillingCharge | null;
|
|
491
|
+
/** The same figures in every currency we bill in, keyed by currency code. */
|
|
492
|
+
charges: Record<string, BillingCharge> | null;
|
|
493
|
+
}
|
|
494
|
+
export interface BillingSubscription {
|
|
495
|
+
id: SubscriptionId;
|
|
496
|
+
plan: PlanId;
|
|
497
|
+
status: SubscriptionStatus;
|
|
498
|
+
currency: string;
|
|
499
|
+
amountMinor: number;
|
|
500
|
+
interval: 'month' | 'year';
|
|
501
|
+
currentPeriodEnd: string | null;
|
|
502
|
+
graceEndsAt: string | null;
|
|
503
|
+
cancelAtPeriodEnd: boolean;
|
|
504
|
+
/** Only ever present while the subscription is `incomplete`. */
|
|
505
|
+
checkoutUrl: string | null;
|
|
506
|
+
}
|
|
507
|
+
export interface BillingProfile {
|
|
508
|
+
legalName: string | null;
|
|
509
|
+
billingEmail: string | null;
|
|
510
|
+
country: string | null;
|
|
511
|
+
taxIdKind: 'gst' | 'vat' | 'abn' | 'other' | null;
|
|
512
|
+
taxId: string | null;
|
|
513
|
+
currency: string | null;
|
|
514
|
+
}
|
|
515
|
+
export interface BillingPayment {
|
|
516
|
+
id: PaymentId;
|
|
517
|
+
kind: string;
|
|
518
|
+
status: PaymentStatus;
|
|
519
|
+
currency: string;
|
|
520
|
+
amountMinor: number;
|
|
521
|
+
taxAmountMinor: number;
|
|
522
|
+
method: string | null;
|
|
523
|
+
paidAt: string | null;
|
|
524
|
+
createdAt: string;
|
|
525
|
+
failureReason: string | null;
|
|
526
|
+
/** Null for a payment that failed — money that did not move has no document. */
|
|
527
|
+
invoiceId: InvoiceId | null;
|
|
528
|
+
invoiceNumber: string | null;
|
|
529
|
+
}
|
|
530
|
+
export interface Billing {
|
|
531
|
+
plan: {
|
|
532
|
+
id: PlanId;
|
|
533
|
+
name: string;
|
|
534
|
+
priceUsd: number;
|
|
535
|
+
};
|
|
536
|
+
subscription: BillingSubscription | null;
|
|
537
|
+
profile: BillingProfile | null;
|
|
538
|
+
payments: BillingPayment[];
|
|
539
|
+
plans: BillingPlan[];
|
|
540
|
+
/** False when the deployment has no payment provider configured. */
|
|
541
|
+
configured: boolean;
|
|
542
|
+
}
|
|
543
|
+
export interface BillingEvent {
|
|
544
|
+
seq: number;
|
|
545
|
+
type: string;
|
|
546
|
+
actor: string;
|
|
547
|
+
reason: string | null;
|
|
548
|
+
detail: Record<string, unknown>;
|
|
549
|
+
occurredAt: string;
|
|
550
|
+
hash: string;
|
|
551
|
+
}
|
|
552
|
+
export interface BillingHistory {
|
|
553
|
+
/** The chain replayed in the database, so the record can be checked. */
|
|
554
|
+
chain: {
|
|
555
|
+
valid: boolean;
|
|
556
|
+
brokenAt: number | null;
|
|
557
|
+
reason: string | null;
|
|
558
|
+
};
|
|
559
|
+
data: BillingEvent[];
|
|
560
|
+
}
|
|
561
|
+
export interface Invoice {
|
|
562
|
+
id: InvoiceId;
|
|
563
|
+
/** The number quoted to an accountant, e.g. `PH-2026-27-0001`. */
|
|
564
|
+
number: string;
|
|
565
|
+
issuedAt: string;
|
|
566
|
+
currency: string;
|
|
567
|
+
subtotalMinor: number;
|
|
568
|
+
taxMinor: number;
|
|
569
|
+
taxRateBp: number | null;
|
|
570
|
+
totalMinor: number;
|
|
571
|
+
paidMinor: number;
|
|
572
|
+
plan: string;
|
|
573
|
+
periodStart: string | null;
|
|
574
|
+
periodEnd: string | null;
|
|
575
|
+
buyer: {
|
|
576
|
+
name: string;
|
|
577
|
+
email: string | null;
|
|
578
|
+
country: string | null;
|
|
579
|
+
taxId: string | null;
|
|
580
|
+
};
|
|
581
|
+
payment: {
|
|
582
|
+
method: string | null;
|
|
583
|
+
reference: string | null;
|
|
584
|
+
paidAt: string | null;
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
/** The single-invoice fetch carries the supplier block the list omits. */
|
|
588
|
+
export interface InvoiceDetail extends Invoice {
|
|
589
|
+
supplier: {
|
|
590
|
+
name: string;
|
|
591
|
+
lines: string[];
|
|
592
|
+
};
|
|
593
|
+
}
|