@posthaste/sdk 0.1.0 → 0.3.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 CHANGED
@@ -89,6 +89,91 @@ await posthaste.emails.send({
89
89
  });
90
90
  ```
91
91
 
92
+ ### Attachments
93
+
94
+ Up to 10 files per message, 10 MiB combined once decoded. `content` accepts a `Uint8Array` — the
95
+ SDK base64-encodes it — or a string that is already base64, sent as-is and never re-encoded (so it
96
+ is never corrupted by double-encoding).
97
+
98
+ ```ts
99
+ await posthaste.emails.send({
100
+ from: 'Acme <billing@acme.com>',
101
+ to: 'customer@example.com',
102
+ subject: 'Your receipt',
103
+ html: '<p>Thanks for your order.</p>',
104
+ attachments: [
105
+ { filename: 'receipt.pdf', contentType: 'application/pdf', content: pdfBytes }, // Uint8Array
106
+ ],
107
+ });
108
+ ```
109
+
110
+ An inline image is the same shape with two extra fields — `disposition: 'inline'` and a `cid`,
111
+ referenced from the html as `cid:logo`:
112
+
113
+ ```ts
114
+ await posthaste.emails.send({
115
+ from: 'Acme <billing@acme.com>',
116
+ to: 'customer@example.com',
117
+ subject: 'Welcome',
118
+ html: '<p>Hello <img src="cid:logo"></p>',
119
+ attachments: [
120
+ {
121
+ filename: 'logo.png',
122
+ contentType: 'image/png',
123
+ content: logoBytes,
124
+ disposition: 'inline',
125
+ cid: 'logo',
126
+ },
127
+ ],
128
+ });
129
+ ```
130
+
131
+ Executable file types (`.exe`, `.js`, `.bat`, `.vbs`, `.msi`, and the rest of Gmail's blocked set)
132
+ are refused with `attachment_type_blocked`, checked against the **final** extension — so
133
+ `invoice.pdf.exe` is blocked. Read a stored attachment's bytes back with `downloadAttachment`:
134
+
135
+ ```ts
136
+ const stored = message.content!.attachments[0]!; // metadata: id, filename, contentType, …
137
+ const file = await posthaste.messages.downloadAttachment(message.id, stored.id);
138
+ file.bytes; // Uint8Array — the exact bytes that were sent
139
+ file.contentType; // an inert type on a small allowlist, or application/octet-stream
140
+ ```
141
+
142
+ ### Scheduling
143
+
144
+ Add `scheduledAt` to send later instead of immediately. It must be **ISO-8601 with a timezone
145
+ offset** — `2026-08-25T09:00:00-05:00`, not `"tomorrow 9am"` — because that is unambiguous and it
146
+ is what `Date.toISOString()` already produces.
147
+
148
+ ```ts
149
+ const result = await posthaste.emails.send({
150
+ from: 'Acme <billing@acme.com>',
151
+ to: 'customer@example.com',
152
+ subject: 'Renewal reminder',
153
+ text: 'Your plan renews in three days.',
154
+ scheduledAt: '2026-08-25T09:00:00-05:00',
155
+ });
156
+
157
+ result.status; // 'scheduled'
158
+ result.scheduledAt; // '2026-08-25T14:00:00.000Z' — normalized to UTC
159
+ ```
160
+
161
+ A timestamp within 60 seconds of now collapses to an ordinary immediate send. Beyond that, how far
162
+ ahead you can go depends on your plan, bounded platform-wide so nothing can schedule past the
163
+ 30-day retention window; going past it throws `schedule_too_far` carrying `maxDays`, the limit that
164
+ actually applied.
165
+
166
+ Quota — suppression, domain verification, your daily and monthly allowance — is decided in full on
167
+ the day the mail actually leaves, not the day you scheduled it. Cancel any time before then:
168
+
169
+ ```ts
170
+ await posthaste.emails.cancelSchedule(result.id);
171
+ // { id, status: 'canceled' } — a retry after a lost response returns the same success again
172
+ ```
173
+
174
+ A cancel attempted after the message has already released throws `not_scheduled`, carrying the
175
+ message's current `status`.
176
+
92
177
  ### Two success statuses, and why you are told which
93
178
 
94
179
  The API answers a **new** send with `202 { status: 'queued' }` and an **idempotency replay** with
@@ -143,32 +228,52 @@ Details worth knowing:
143
228
 
144
229
  ## Pagination
145
230
 
146
- Lists are keyset-paginated: `limit` and `before` in, `{ data, hasMore, nextCursor }` out. `before`
147
- is the id of the last row you received, which is exactly what `nextCursor` gives you.
231
+ **Every list is paginated.** `limit` and `before` in, `{ data, nextCursor }` out — plus `hasMore` on
232
+ the three endpoints that send it. `before` is the id of the last row you received, which is exactly
233
+ what `nextCursor` gives you.
148
234
 
149
235
  ```ts
150
236
  const page = await posthaste.messages.list({ limit: 50, status: 'bounced' });
151
237
  // page.data, page.hasMore, page.nextCursor
152
238
  ```
153
239
 
154
- ### Loop on `hasMore`, never on `nextCursor`
240
+ | Resource | `limit` | Cursor | `hasMore`? |
241
+ | ------------------ | ------------------ | ----------- | ---------- |
242
+ | `messages` | 1–100, default 25 | `before` | yes |
243
+ | `suppressions` | 1–200, default 50 | `before` | yes |
244
+ | `domains` | 1–100, default 100 | `before` | **no** |
245
+ | `webhooks` | 1–100, default 50 | `before` | **no** |
246
+ | `apiKeys` | 1–100, default 50 | `before` | **no** |
247
+ | `billing.invoices` | 1–100, default 50 | `before` | **no** |
248
+ | `billing.history` | 1–200, default 100 | **`after`** | **no** |
249
+
250
+ ### Check both signals, and neither on its own
155
251
 
156
- `nextCursor` is derived from the last row of a page, and some endpoints return a **non-null cursor
157
- on the final page**. The obvious loop —
252
+ The two obvious loops each break on a different endpoint, in opposite directions.
158
253
 
159
254
  ```ts
160
- // WRONG. This can never terminate.
255
+ // WRONG, way one. This can never terminate.
256
+ // `nextCursor` is derived from the last row of a page, and /v1/inbound/messages
257
+ // returns a non-null cursor on its FINAL page — so this asks for the page after
258
+ // the last one, gets an empty page carrying the same cursor, and spins for ever.
161
259
  let cursor: string | null | undefined = undefined;
162
260
  do {
163
261
  const page = await posthaste.messages.list({ before: cursor });
164
262
  cursor = page.nextCursor;
165
263
  } while (cursor);
264
+
265
+ // WRONG, way two. This stops after ONE page.
266
+ // domains, webhooks, apiKeys, billing.invoices and billing.history send no
267
+ // `hasMore` at all, so this reads `undefined`, treats it as "that was
268
+ // everything", and silently truncates. Compare with `=== false`.
269
+ if (!page.hasMore) return;
166
270
  ```
167
271
 
168
- asks for the page after the last one, gets an empty page carrying the same cursor back, and spins
169
- for ever. Nothing errors; it just never finishes.
272
+ Neither failure produces an error. The first hangs; the second quietly gives you a partial list
273
+ sixty invoices behind a default of fifty is fifty invoices and no way to tell.
170
274
 
171
- `autoPaginate` encodes the correct condition, so you cannot get it wrong:
275
+ `autoPaginate` consults both signals and treats each as authoritative only where the server actually
276
+ sends it, so you cannot get either wrong:
172
277
 
173
278
  ```ts
174
279
  for await (const message of posthaste.messages.autoPaginate({ status: 'bounced' })) {
@@ -176,8 +281,8 @@ for await (const message of posthaste.messages.autoPaginate({ status: 'bounced'
176
281
  }
177
282
  ```
178
283
 
179
- It stops on `hasMore === false`, and also stops on an empty page or a cursor that fails to advance —
180
- so a contract change upstream produces a short result, never an infinite loop.
284
+ It stops on `hasMore === false`, on a null cursor, on an empty page, and on a cursor that fails to
285
+ advance — so a contract change upstream produces a short result, never an infinite loop.
181
286
 
182
287
  For the common "give me an array" case, with a ceiling you choose:
183
288
 
@@ -185,8 +290,31 @@ For the common "give me an array" case, with a ceiling you choose:
185
290
  const recent = await posthaste.messages.listAll({ status: 'bounced' }, 500);
186
291
  ```
187
292
 
188
- `suppressions` has the same three methods. `domains`, `webhooks`, `apiKeys` and
189
- `billing.invoices` are not paginated — they return `{ data }` whole, capped server-side at 100.
293
+ `messages`, `suppressions`, `domains`, `webhooks` and `apiKeys` all have the same three methods
294
+ (`list`, `autoPaginate`, `listAll`). `billing` names its two lists explicitly:
295
+ `invoices` / `autoPaginateInvoices` / `listAllInvoices`, and `history` / `autoPaginateHistory` /
296
+ `listAllHistory`.
297
+
298
+ ### `billing.history` is the exception
299
+
300
+ It is the one list that reads **forward**, and it does not take `before` at all. The billing record
301
+ is a hash chain, a chain is verified from its start, and a record that renders in a different order
302
+ than it verifies in is one people stop trusting — so events come back **oldest first** and you
303
+ resume with `after`.
304
+
305
+ `after` is a **sequence number** (a `BillingEvent.seq`), not a prefixed id; passing an id is a
306
+ `400 invalid_request`. `nextCursor` is that number as a string, so passing it straight back is
307
+ correct.
308
+
309
+ ```ts
310
+ for await (const event of posthaste.billing.autoPaginateHistory()) {
311
+ console.log(event.seq, event.type);
312
+ }
313
+ ```
314
+
315
+ `chain` travels with every page and verifies the **whole account history**, not the page you are
316
+ holding — so it means the same thing on page four as on page one. `autoPaginateHistory` yields only
317
+ the events; call `history()` if you need the verification too.
190
318
 
191
319
  ---
192
320
 
@@ -198,7 +326,7 @@ Register an endpoint and Posthaste posts events to it as they happen. The signin
198
326
  ```ts
199
327
  const hook = await posthaste.webhooks.create({
200
328
  url: 'https://acme.com/hooks/posthaste',
201
- eventTypes: ['delivered', 'bounced', 'complained'], // omit for all ten
329
+ eventTypes: ['delivered', 'bounced', 'complained'], // omit for all thirteen
202
330
  });
203
331
 
204
332
  await store(hook.signingSecret); // whsec_… — you cannot read it again
@@ -271,8 +399,9 @@ verifyWebhook(rawBody, header, secret, { toleranceSeconds: 300 });
271
399
  `parseWebhookEvent` verifies and JSON-parses in one step, returning `null` on any failure, for the
272
400
  common case where a bad delivery just gets a 400.
273
401
 
274
- The ten event types are `accepted`, `queued`, `attempted`, `delivered`, `deferred`, `bounced`,
275
- `complained`, `failed`, `rejected` and `suppressed` — exported as `EVENT_TYPES`.
402
+ The thirteen event types are `accepted`, `queued`, `attempted`, `delivered`, `deferred`, `bounced`,
403
+ `complained`, `failed`, `rejected`, `suppressed`, `scheduled`, `schedule_canceled` and
404
+ `schedule_failed` — exported as `EVENT_TYPES`.
276
405
 
277
406
  ---
278
407
 
@@ -307,7 +436,8 @@ switch (err.type) {
307
436
  return permanent(err); // do not retry — fix the request
308
437
 
309
438
  case 'rate_limited':
310
- return retryAfter(err.retryAfterSeconds ?? 60); // seconds; transient
439
+ case 'platform_paused':
440
+ return retryAfter(err.retryAfterSeconds ?? 60); // transient; come back
311
441
 
312
442
  case 'daily_limit_reached':
313
443
  case 'monthly_limit_reached':
@@ -343,21 +473,30 @@ server sends one.
343
473
 
344
474
  Two rules make this safe rather than merely automatic.
345
475
 
346
- **It branches on `error.type`, not on the status.** Three different refusals arrive as `429`:
476
+ **It branches on `error.type`, not on the status.** Four different refusals arrive as `429`:
347
477
 
348
- | type | what it means | `Retry-After` | retried? |
349
- | ----------------------- | ----------------------------------- | -------------- | -------- |
350
- | `rate_limited` | too many requests for this key | seconds | yes |
351
- | `daily_limit_reached` | today's warmup cap is spent | until midnight | **no** |
352
- | `monthly_limit_reached` | the plan's monthly allowance is out | up to a month | **no** |
478
+ | type | what it means | `Retry-After` | classified as | retried? |
479
+ | ----------------------- | ----------------------------------- | -------------- | ------------------ | --------- |
480
+ | `rate_limited` | too many requests for this key | seconds | `isRateLimited` | yes |
481
+ | `platform_paused` | the PLATFORM's daily send ceiling | a few minutes | `isRateLimited` | see below |
482
+ | `daily_limit_reached` | today's warmup cap is spent | until midnight | `isQuotaExhausted` | **no** |
483
+ | `monthly_limit_reached` | the plan's monthly allowance is out | up to a month | `isQuotaExhausted` | **no** |
353
484
 
354
- The last two are exhausted quota, not throttling. Retrying them in-process would hammer a wall the
485
+ The bottom two are exhausted quota, not throttling. Retrying them in-process would hammer a wall the
355
486
  calendar has to move before it opens, while burning your request-rate limit on the way. They are
356
487
  raised immediately, with `err.isQuotaExhausted === true` and the wait on
357
488
  `err.retryAfterSeconds`, so _you_ can decide — a queue, a delay, an alert.
358
489
 
359
- A `Retry-After` longer than `maxRetryDelayMs` (60 seconds by default) is also not slept through:
360
- blocking a request handler for fifteen minutes is indistinguishable from a hang.
490
+ `platform_paused` looks like them and is not one of them. It says the platform's own daily total is
491
+ full, not that you have spent anything: nothing about your account changes it, upgrading does not
492
+ clear it, and it frees as the day's total drains rather than on a calendar boundary. So it is
493
+ `isRateLimited`, never `isQuotaExhausted` — tell a customer to come back, not to upgrade.
494
+
495
+ A `Retry-After` longer than `maxRetryDelayMs` (60 seconds by default) is not slept through, whatever
496
+ its type: blocking a request handler for fifteen minutes is indistinguishable from a hang. The real
497
+ `platform_paused` ceiling sends about five minutes, so in practice it comes back to you to schedule —
498
+ that is `maxRetryDelayMs` judging the length of the wait, not the quota rule, and raising
499
+ `maxRetryDelayMs` makes the SDK sit through it.
361
500
 
362
501
  **It never retries a request that repeating could duplicate.** Concretely, a send is retried **only
363
502
  when you supplied an `idempotencyKey`**, and `webhooks.create` is never retried (a duplicate
@@ -377,7 +516,9 @@ account.verify() GET /v1/account/verify
377
516
  account.usage() GET /v1/usage
378
517
 
379
518
  domains.create({ name }) POST /v1/domains
380
- domains.list() GET /v1/domains
519
+ domains.list(params) GET /v1/domains
520
+ domains.autoPaginate(params) GET /v1/domains (all pages)
521
+ domains.listAll(params, maxItems) GET /v1/domains (all pages)
381
522
  domains.verify(id) POST /v1/domains/:id/verify
382
523
  domains.delete(id) DELETE /v1/domains/:id
383
524
  domains.setup(id) GET /v1/domains/:id/setup
@@ -385,11 +526,13 @@ domains.connectCloudflare(id, { token }) POST /v1/domains/:id/cloudflare
385
526
  domains.disconnectCloudflare() DELETE /v1/account/cloudflare
386
527
 
387
528
  emails.send(params) POST /v1/emails
529
+ emails.cancelSchedule(id) DELETE /v1/emails/:id/schedule
388
530
 
389
531
  messages.list(params) GET /v1/messages
390
532
  messages.autoPaginate(params) GET /v1/messages (all pages)
391
533
  messages.listAll(params, maxItems) GET /v1/messages (all pages)
392
534
  messages.get(id) GET /v1/messages/:id
535
+ messages.downloadAttachment(msgId, attId) GET /v1/messages/:id/attachments/:attachmentId
393
536
  messages.stats(params) GET /v1/stats/messages
394
537
 
395
538
  suppressions.list(params) GET /v1/suppressions
@@ -399,14 +542,22 @@ suppressions.create({ address, reason }) POST /v1/suppressions
399
542
  suppressions.delete(address) DELETE /v1/suppressions/:address
400
543
 
401
544
  webhooks.create({ url, eventTypes }) POST /v1/webhooks
402
- webhooks.list() GET /v1/webhooks
545
+ webhooks.list(params) GET /v1/webhooks
546
+ webhooks.autoPaginate(params) GET /v1/webhooks (all pages)
547
+ webhooks.listAll(params, maxItems) GET /v1/webhooks (all pages)
403
548
  webhooks.delete(id) DELETE /v1/webhooks/:id
404
549
 
405
- apiKeys.list() GET /v1/api-keys
550
+ apiKeys.list(params) GET /v1/api-keys
551
+ apiKeys.autoPaginate(params) GET /v1/api-keys (all pages)
552
+ apiKeys.listAll(params, maxItems) GET /v1/api-keys (all pages)
406
553
 
407
554
  billing.get() GET /v1/billing
408
- billing.history() GET /v1/billing/history
409
- billing.invoices() GET /v1/billing/invoices
555
+ billing.history(params) GET /v1/billing/history (`after`, oldest first)
556
+ billing.autoPaginateHistory(params) GET /v1/billing/history (all pages)
557
+ billing.listAllHistory(params, maxItems) GET /v1/billing/history (all pages)
558
+ billing.invoices(params) GET /v1/billing/invoices
559
+ billing.autoPaginateInvoices(params) GET /v1/billing/invoices (all pages)
560
+ billing.listAllInvoices(params, maxItems) GET /v1/billing/invoices (all pages)
410
561
  billing.invoice(id) GET /v1/billing/invoices/:id
411
562
  ```
412
563
 
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Pure base64 encoder for attachment bytes.
3
+ *
4
+ * Hand-rolled on purpose: this package ships with zero dependencies (enforced
5
+ * by packaging.test.ts), `Buffer` is Node-only and this SDK also runs bundled
6
+ * in edge runtimes, and `btoa` chokes on code points over 0xff unless the
7
+ * input is first mangled through a binary string. Fifteen lines beats any of
8
+ * those failure modes.
9
+ */
10
+ export declare function bytesToBase64(bytes: Uint8Array): string;
package/dist/base64.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pure base64 encoder for attachment bytes.
3
+ *
4
+ * Hand-rolled on purpose: this package ships with zero dependencies (enforced
5
+ * by packaging.test.ts), `Buffer` is Node-only and this SDK also runs bundled
6
+ * in edge runtimes, and `btoa` chokes on code points over 0xff unless the
7
+ * input is first mangled through a binary string. Fifteen lines beats any of
8
+ * those failure modes.
9
+ */
10
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
11
+ export function bytesToBase64(bytes) {
12
+ let out = '';
13
+ for (let i = 0; i < bytes.length; i += 3) {
14
+ const a = bytes[i];
15
+ const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
16
+ const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
17
+ out += ALPHABET[a >> 2] + ALPHABET[((a & 0x03) << 4) | (b >> 4)];
18
+ out += i + 1 < bytes.length ? ALPHABET[((b & 0x0f) << 2) | (c >> 6)] : '=';
19
+ out += i + 2 < bytes.length ? ALPHABET[c & 0x3f] : '=';
20
+ }
21
+ return out;
22
+ }
package/dist/client.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * is absent because a customer cannot use it — see the README.
13
13
  */
14
14
  import { HttpClient, type PosthasteOptions, type RequestOptions } from './http.js';
15
- import type { Account, Billing, BillingHistory, ChainVerification, CloudflarePublishResult, ConnectCloudflareParams, CreateDomainParams, CreateSuppressionParams, CreateWebhookParams, CreatedDomain, CreatedSuppression, CreatedWebhook, Domain, DomainSetup, DomainVerification, Invoice, InvoiceDetail, List, ApiKey, ListMessagesParams, ListSuppressionsParams, Message, MessageStats, MessageStatsParams, MessageSummary, Page, SendEmailParams, SendEmailResult, Suppression, Usage, Webhook } from './types.js';
15
+ import type { Account, Billing, BillingEvent, BillingHistory, ChainVerification, CloudflarePublishResult, ConnectCloudflareParams, CreateDomainParams, CreateSuppressionParams, CreateWebhookParams, CreatedDomain, CreatedSuppression, CreatedWebhook, CursorPage, Domain, DomainSetup, DomainVerification, Invoice, InvoiceDetail, ApiKey, ListApiKeysParams, ListBillingHistoryParams, ListDomainsParams, ListInvoicesParams, ListMessagesParams, ListSuppressionsParams, ListWebhooksParams, Message, MessageStats, MessageStatsParams, MessageSummary, Page, SendEmailParams, SendEmailResult, Suppression, Usage, Webhook } from './types.js';
16
16
  export declare class Posthaste {
17
17
  private readonly http;
18
18
  readonly account: AccountResource;
@@ -52,8 +52,19 @@ export declare class DomainsResource {
52
52
  * response cannot leave duplicates behind.
53
53
  */
54
54
  create(params: CreateDomainParams, options?: RequestOptions): Promise<CreatedDomain>;
55
- /** `GET /v1/domains` — every domain, newest first. Not paginated. */
56
- list(options?: RequestOptions): Promise<List<Domain>>;
55
+ /**
56
+ * `GET /v1/domains` — one keyset page, newest first.
57
+ *
58
+ * `limit` defaults to 100, which is also its maximum, so on most accounts the
59
+ * first page is every domain. It is still a page: past a hundred domains the
60
+ * rest are behind `nextCursor`, and there is no `hasMore` here to tell you so.
61
+ * Use `autoPaginate` if the count is not bounded by something you control.
62
+ */
63
+ list(params?: ListDomainsParams, options?: RequestOptions): Promise<CursorPage<Domain>>;
64
+ /** Every domain, across every page. */
65
+ autoPaginate(params?: ListDomainsParams, options?: RequestOptions): AsyncGenerator<Domain, void, undefined>;
66
+ /** Drain `autoPaginate` into an array, up to `maxItems`. */
67
+ listAll(params?: ListDomainsParams, maxItems?: number, options?: RequestOptions): Promise<Domain[]>;
57
68
  /**
58
69
  * `POST /v1/domains/:id/verify` — look for the records and record the result.
59
70
  *
@@ -120,6 +131,18 @@ export declare class EmailsResource {
120
131
  * the header.
121
132
  */
122
133
  send(params: SendEmailParams, options?: RequestOptions): Promise<SendEmailResult>;
134
+ /**
135
+ * `DELETE /v1/emails/:id/schedule` — cancel a scheduled send.
136
+ *
137
+ * Succeeds until the message is released to delivery (which happens within
138
+ * about a minute of its scheduled time). A message that already released
139
+ * throws `not_scheduled` carrying its current status; a cancel retried
140
+ * after a lost response returns success again rather than erroring.
141
+ */
142
+ cancelSchedule(id: string, options?: RequestOptions): Promise<{
143
+ id: string;
144
+ status: 'canceled';
145
+ }>;
123
146
  }
124
147
  export declare class MessagesResource {
125
148
  private readonly http;
@@ -140,6 +163,19 @@ export declare class MessagesResource {
140
163
  * linkage, with the chain re-verified on read (`recordIntact`).
141
164
  */
142
165
  get(id: string, options?: RequestOptions): Promise<Message>;
166
+ /**
167
+ * `GET /v1/messages/:id/attachments/:attachmentId` — the stored bytes.
168
+ *
169
+ * The attachment id comes from `get(id).content.attachments[].id`. Returns
170
+ * the exact bytes that were sent; the server serves them with
171
+ * `Content-Disposition: attachment` and only ever an inert content type,
172
+ * echoed here for callers who re-serve the file.
173
+ */
174
+ downloadAttachment(messageId: string, attachmentId: string, options?: RequestOptions): Promise<{
175
+ bytes: Uint8Array;
176
+ contentType: string;
177
+ filename: string | null;
178
+ }>;
143
179
  /**
144
180
  * `GET /v1/stats/messages` — daily volume with the previous window for
145
181
  * comparison.
@@ -195,8 +231,17 @@ export declare class WebhooksResource {
195
231
  * duplicate would then receive every event twice.
196
232
  */
197
233
  create(params: CreateWebhookParams, options?: RequestOptions): Promise<CreatedWebhook>;
198
- /** `GET /v1/webhooks` — newest first. Not paginated. Never includes secrets. */
199
- list(options?: RequestOptions): Promise<List<Webhook>>;
234
+ /**
235
+ * `GET /v1/webhooks` — one keyset page, newest first. Never includes secrets.
236
+ *
237
+ * `limit` defaults to 50 and caps at 100. There is no `hasMore` on this
238
+ * endpoint; `nextCursor` is the end-of-list signal.
239
+ */
240
+ list(params?: ListWebhooksParams, options?: RequestOptions): Promise<CursorPage<Webhook>>;
241
+ /** Every webhook, across every page. */
242
+ autoPaginate(params?: ListWebhooksParams, options?: RequestOptions): AsyncGenerator<Webhook, void, undefined>;
243
+ /** Drain `autoPaginate` into an array, up to `maxItems`. */
244
+ listAll(params?: ListWebhooksParams, maxItems?: number, options?: RequestOptions): Promise<Webhook[]>;
200
245
  /** `DELETE /v1/webhooks/:id` — 204, or 404 for an unknown id. */
201
246
  delete(id: string, options?: RequestOptions): Promise<void>;
202
247
  }
@@ -204,14 +249,22 @@ export declare class ApiKeysResource {
204
249
  private readonly http;
205
250
  constructor(http: HttpClient);
206
251
  /**
207
- * `GET /v1/api-keys` — the 100 most recent keys, revoked ones included.
252
+ * `GET /v1/api-keys` — one keyset page of keys, revoked ones included.
253
+ *
254
+ * `limit` defaults to 50 and caps at 100, and revoked keys count towards it —
255
+ * so an account that has rotated its credentials a few times will have more
256
+ * than one page. No `hasMore`; `nextCursor` is the end-of-list signal.
208
257
  *
209
258
  * Read only, and that is the whole resource. Creating and revoking keys
210
259
  * requires a signed-in owner or admin and refuses a Bearer key outright: a
211
260
  * server-side credential that could mint more credentials would make every
212
261
  * narrow key one request away from a full one.
213
262
  */
214
- list(options?: RequestOptions): Promise<List<ApiKey>>;
263
+ list(params?: ListApiKeysParams, options?: RequestOptions): Promise<CursorPage<ApiKey>>;
264
+ /** Every key, across every page. */
265
+ autoPaginate(params?: ListApiKeysParams, options?: RequestOptions): AsyncGenerator<ApiKey, void, undefined>;
266
+ /** Drain `autoPaginate` into an array, up to `maxItems`. */
267
+ listAll(params?: ListApiKeysParams, maxItems?: number, options?: RequestOptions): Promise<ApiKey[]>;
215
268
  }
216
269
  export declare class BillingResource {
217
270
  private readonly http;
@@ -225,10 +278,48 @@ export declare class BillingResource {
225
278
  * gets you in. Money is always in minor units.
226
279
  */
227
280
  get(options?: RequestOptions): Promise<Billing>;
228
- /** `GET /v1/billing/history` — the hash-chained commercial record, oldest first, plus its verification. */
229
- history(options?: RequestOptions): Promise<BillingHistory>;
230
- /** `GET /v1/billing/invoices` — the 100 most recent, newest first. Not paginated. */
231
- invoices(options?: RequestOptions): Promise<List<Invoice>>;
281
+ /**
282
+ * `GET /v1/billing/history` one page of the hash-chained commercial record,
283
+ * plus its verification.
284
+ *
285
+ * THE ODD ONE OUT, in two ways that a caller who assumes the house style will
286
+ * get wrong:
287
+ *
288
+ * - It reads FORWARD. Events come back oldest first, and the page resumes
289
+ * from `after`, not `before`. That is not a stylistic choice: a hash chain
290
+ * is verified from its start, and a record that renders in a different
291
+ * order than it verifies in is one people stop trusting.
292
+ * - `after` is a SEQUENCE NUMBER — a `BillingEvent.seq` — not a prefixed id.
293
+ * Passing an id gets `400 invalid_request`. `nextCursor` is that number
294
+ * rendered as a string, so passing it straight back is correct.
295
+ *
296
+ * `limit` defaults to 100 and caps at 200. `chain` is verified over the whole
297
+ * account history rather than over the page, so it means the same thing on
298
+ * every page.
299
+ */
300
+ history(params?: ListBillingHistoryParams, options?: RequestOptions): Promise<BillingHistory>;
301
+ /**
302
+ * Every billing event, oldest first, across every page.
303
+ *
304
+ * Note that this yields the EVENTS only. `chain` is per-response, so if you
305
+ * need the verification as well, call `history` and read it from there.
306
+ */
307
+ autoPaginateHistory(params?: ListBillingHistoryParams, options?: RequestOptions): AsyncGenerator<BillingEvent, void, undefined>;
308
+ /** Drain `autoPaginateHistory` into an array, up to `maxItems`. */
309
+ listAllHistory(params?: ListBillingHistoryParams, maxItems?: number, options?: RequestOptions): Promise<BillingEvent[]>;
310
+ /**
311
+ * `GET /v1/billing/invoices` — one keyset page, newest first.
312
+ *
313
+ * `limit` defaults to 50 and caps at 100, and there is no `hasMore` here. An
314
+ * account past its fiftieth invoice that reads only `data` is silently short
315
+ * of its own financial record — follow `nextCursor`, or use
316
+ * `autoPaginateInvoices`.
317
+ */
318
+ invoices(params?: ListInvoicesParams, options?: RequestOptions): Promise<CursorPage<Invoice>>;
319
+ /** Every invoice, across every page. */
320
+ autoPaginateInvoices(params?: ListInvoicesParams, options?: RequestOptions): AsyncGenerator<Invoice, void, undefined>;
321
+ /** Drain `autoPaginateInvoices` into an array, up to `maxItems`. */
322
+ listAllInvoices(params?: ListInvoicesParams, maxItems?: number, options?: RequestOptions): Promise<Invoice[]>;
232
323
  /** `GET /v1/billing/invoices/:id` — the same document plus the supplier block. */
233
324
  invoice(id: string, options?: RequestOptions): Promise<InvoiceDetail>;
234
325
  }