@checkoutpage/sdk 0.1.31 → 0.1.33

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
@@ -127,6 +127,40 @@ const results = await checkoutpage.coupons.create({
127
127
  });
128
128
  ```
129
129
 
130
+ Without `rules` a coupon discounts the whole order on every checkout page and event. `rules`
131
+ limits it, one rule per checkout page or event, combined with OR:
132
+
133
+ - Checkout pages: `wholeOrder`, `prices` (only when the customer buys one of `priceIds`, and
134
+ only that price is discounted) or `pricesAndBumps` (only when the customer buys one of
135
+ `priceIds` **and** one of `bumpProductIds`, and both are discounted).
136
+ - Events: `wholeOrder`, `tickets` or `ticketsAndAddOns`, the same with `ticketTypeIds` and
137
+ add-ons.
138
+ - An empty or missing `priceIds`, `ticketTypeIds` or `bumpProductIds` means all of them,
139
+ including ones added later. `priceIds` are the `id`s of the page's main product `prices`, not
140
+ Stripe price ids.
141
+
142
+ ```typescript
143
+ const results = await checkoutpage.coupons.create({
144
+ type: 'percent',
145
+ label: 'Monthly plus workbook',
146
+ code: 'BUNDLE20',
147
+ percentOff: 20,
148
+ duration: 'once',
149
+ rules: [
150
+ {
151
+ pageId,
152
+ appliesTo: 'pricesAndBumps',
153
+ priceIds: [monthlyPriceId],
154
+ bumpProductIds: [workbookProductId],
155
+ },
156
+ { pageId: eventId, appliesTo: 'tickets', ticketTypeIds: [vipTicketTypeId] },
157
+ ],
158
+ });
159
+ ```
160
+
161
+ `pageIds` and `ticketTypeIds` are deprecated in favour of `rules`. They still work, but only
162
+ without `rules`, and responses derive them from the rules.
163
+
130
164
  ### Subscriptions
131
165
 
132
166
  #### List subscriptions
@@ -241,6 +275,67 @@ const pageBookings = await checkoutpage.bookings.list({
241
275
  });
242
276
  ```
243
277
 
278
+ #### Create a booking
279
+
280
+ `bookings.create` issues a real booking without collecting card payment: tickets
281
+ are issued, the customer gets a confirmation email with the ticket PDF, and
282
+ booking webhooks fire. Field values are addressed by `fieldId`, and every field
283
+ the event marks as required must be supplied.
284
+
285
+ ```typescript
286
+ const { data: booking } = await checkoutpage.bookings.create({
287
+ eventId: '67fcbdac6a91c25ef2d3534a',
288
+ tickets: { '65f4a1c2e4a9f3d2b1c0a9e8': 2 },
289
+ fields: [
290
+ { fieldId: '507f1f77bcf86cd799439030', value: 'ada@example.com' },
291
+ { fieldId: '507f1f77bcf86cd799439031', value: 'Ada Lovelace' },
292
+ ],
293
+ paymentOption: { manualType: 'invoice' },
294
+ });
295
+
296
+ console.log(booking.status); // 'unpaid'
297
+ console.log(booking.amountDue); // 5000, settled outside checkout
298
+ ```
299
+
300
+ #### Create a complimentary booking
301
+
302
+ There are two ways to settle a booking, and exactly one of them is required.
303
+ Pass `paymentOption`, as above, and the booking is recorded as `unpaid` with the
304
+ amount as its balance due, settled outside checkout. Pass `complimentary: true`
305
+ instead and the tickets are issued at no charge: the booking is recorded as
306
+ `paid` with `amount`, `amountPaid` and `amountDue` of 0 and
307
+ `isComplimentary: true`, while the ticket lines keep their face value and
308
+ `complimentaryDiscountAmount` records what they would have cost. `complimentary`
309
+ cannot be combined with `paymentOption` or `couponId`.
310
+
311
+ ```typescript
312
+ const { data: booking } = await checkoutpage.bookings.create({
313
+ eventId: '67fcbdac6a91c25ef2d3534a',
314
+ tickets: { '65f4a1c2e4a9f3d2b1c0a9e8': 2 },
315
+ fields: [
316
+ { fieldId: '507f1f77bcf86cd799439030', value: 'ada@example.com' },
317
+ { fieldId: '507f1f77bcf86cd799439031', value: 'Ada Lovelace' },
318
+ ],
319
+ complimentary: true,
320
+ });
321
+
322
+ console.log(booking.status); // 'paid'
323
+ console.log(booking.amount); // 0
324
+ console.log(booking.complimentaryDiscountAmount); // 5000 (2 x 2500 face value)
325
+ ```
326
+
327
+ #### Filter complimentary bookings
328
+
329
+ ```typescript
330
+ const complimentary = await checkoutpage.bookings.list({
331
+ isComplimentary: 'true',
332
+ });
333
+
334
+ const paidFor = await checkoutpage.bookings.list({
335
+ isComplimentary: 'false',
336
+ });
337
+ ```
338
+
244
339
  ### Products
245
340
 
246
341
  #### Get a product
@@ -249,6 +344,15 @@ const pageBookings = await checkoutpage.bookings.list({
249
344
  const product = await checkoutpage.products.get('product_id');
250
345
  ```
251
346
 
347
+ #### List products
348
+
349
+ ```typescript
350
+ const products = await checkoutpage.products.list({
351
+ search: 'shirt',
352
+ limit: 20,
353
+ });
354
+ ```
355
+
252
356
  #### Update a product
253
357
 
254
358
  ```typescript
@@ -299,6 +403,64 @@ await checkoutpage.checkoutPages.create({
299
403
  > report them by mirroring the product — but new integrations should set them
300
404
  > under `productData` or via `products.update()`.
301
405
 
406
+ ### Order bumps
407
+
408
+ An order bump is an extra product offered on a checkout page or event (shown as
409
+ an add-on on an event). Create the order bump product once, then offer it on as
410
+ many pages as you like.
411
+
412
+ #### Create an order bump product
413
+
414
+ Order bump products use a single one-time price, which can have a setup fee or be
415
+ pay what you want. Recurring prices and payment plans are refused with a
416
+ `ValidationError`.
417
+
418
+ ```typescript
419
+ const { data: bump } = await checkoutpage.products.create({
420
+ role: 'orderbump',
421
+ title: 'Gift wrapping',
422
+ price: { amount: 500, currency: 'usd' },
423
+ callToActionText: 'Yes! Add gift wrapping',
424
+ shortDescription: 'Wrapped in recycled paper with a handwritten note.',
425
+ });
426
+
427
+ const bumps = await checkoutpage.products.list({ role: 'orderbump' });
428
+ ```
429
+
430
+ #### Offer it on a page
431
+
432
+ The same methods are on `checkoutpage.events.orderBumps` for events.
433
+
434
+ ```typescript
435
+ await checkoutpage.checkoutPages.orderBumps.create('page_id', {
436
+ productId: bump.id,
437
+ preselected: true,
438
+ });
439
+
440
+ const { data: placed } = await checkoutpage.checkoutPages.orderBumps.list('page_id');
441
+
442
+ await checkoutpage.checkoutPages.orderBumps.update('page_id', bump.id, {
443
+ allowQuantity: true,
444
+ });
445
+
446
+ // List every order bump on the page, once, in the new order
447
+ await checkoutpage.checkoutPages.orderBumps.reorder('page_id', {
448
+ productIds: ['other_bump_id', bump.id],
449
+ });
450
+
451
+ // Stops offering it on this page; the product is kept
452
+ await checkoutpage.checkoutPages.orderBumps.delete('page_id', bump.id);
453
+ ```
454
+
455
+ #### Delete an order bump product
456
+
457
+ Only order bump products that have never been bought can be deleted. A product
458
+ still offered on a page is refused unless you pass `fromAllPages: true`.
459
+
460
+ ```typescript
461
+ await checkoutpage.products.delete(bump.id, { fromAllPages: true });
462
+ ```
463
+
302
464
  ### Files
303
465
 
304
466
  #### Upload an image
@@ -327,6 +489,115 @@ const result = await checkoutpage.files.upload({
327
489
  console.log('File uploaded:', result.data.id);
328
490
  ```
329
491
 
492
+ ### Themes
493
+
494
+ A theme is a page design you can reuse across checkout pages, events and forms. `list()` returns
495
+ your own themes (`scope: 'seller'`) and the built-in ones (`scope: 'global'`), which are read-only.
496
+
497
+ #### List themes
498
+
499
+ ```typescript
500
+ const { data: themes, has_more } = await checkoutpage.themes.list();
501
+
502
+ const builtIn = await checkoutpage.themes.list({ scope: 'global' });
503
+ ```
504
+
505
+ #### Create a theme
506
+
507
+ ```typescript
508
+ const { data: theme } = await checkoutpage.themes.create({
509
+ name: 'Brand',
510
+ // Optional: start from a copy of a built-in theme or one of yours
511
+ baseThemeId: builtIn.data[0].id,
512
+ variables: { tokens: { colorPrimary: { light: '#4f46e5' } } },
513
+ });
514
+ ```
515
+
516
+ `variables` takes the same shape as a page's `themeOverrides`. A copy of a built-in theme does not
517
+ follow your store's brand colour, so set `tokens.colorPrimary` if you want it. A name another of
518
+ your themes already uses is rejected with a `ConflictError`.
519
+
520
+ #### Get and update a theme
521
+
522
+ ```typescript
523
+ const { data: theme } = await checkoutpage.themes.get(themeId);
524
+
525
+ const { data: updated } = await checkoutpage.themes.update(themeId, {
526
+ name: 'Brand v2',
527
+ variables: { tokens: { colorPrimary: { light: '#0f766e' }, radius: null } },
528
+ });
529
+ ```
530
+
531
+ Every page that uses the theme changes too. Keys you leave out keep their value, and `null` resets a
532
+ property, or everything under a key. Built-in themes can't be changed: copy one with `baseThemeId`.
533
+
534
+ #### Use a theme on a page
535
+
536
+ ```typescript
537
+ await checkoutpage.checkoutPages.update(pageId, { themeId: theme.id });
538
+ ```
539
+
540
+ `events` and `forms` take `themeId` on `create` and `update` too, and page responses include it.
541
+ Changing a page's theme replaces the page's own theme settings, including ones set in the
542
+ dashboard such as custom CSS, so the page looks like the theme. `themeOverrides` sent in the same
543
+ request apply on top, and the page's locale is kept.
544
+
545
+ #### Delete a theme
546
+
547
+ ```typescript
548
+ const { data: deleted } = await checkoutpage.themes.delete(themeId);
549
+ ```
550
+
551
+ A theme that any of your pages uses, archived ones included, or that is your store's default theme,
552
+ can't be deleted and throws a `ConflictError`. Move those pages to another theme with `themeId`
553
+ first.
554
+
555
+ ### Form layout
556
+
557
+ `formLayout` arranges a page's form, top to bottom: fields, headings, dividers, the discount code
558
+ box and groups that put up to 4 items side by side. Checkout pages, events and forms take it on
559
+ `create` and `update`, and page responses always include it. On `create`, reference a field by
560
+ `fieldKey`, the `key` you gave it in `fields` in the same request.
561
+
562
+ ```typescript
563
+ import type { CreateFormLayout } from '@checkoutpage/sdk';
564
+
565
+ const formLayout: CreateFormLayout = [
566
+ { type: 'heading', id: 'your-details', text: 'Your details', level: 'heading' },
567
+ {
568
+ type: 'group',
569
+ id: 'name-row',
570
+ columns: 2,
571
+ items: [
572
+ { type: 'field', fieldKey: 'first-name' },
573
+ { type: 'field', fieldKey: 'last-name' },
574
+ ],
575
+ },
576
+ { type: 'divider', id: 'divider-1' },
577
+ { type: 'discount-code' },
578
+ ];
579
+
580
+ const { data: page } = await checkoutpage.checkoutPages.create({
581
+ name: 'Workshop',
582
+ productData: { title: 'Workshop', price: { amount: 4900, currency: 'usd' } },
583
+ fields: [
584
+ { label: 'Email', element: 'email', type: 'email', required: true },
585
+ { key: 'first-name', element: 'text', label: 'First name' },
586
+ { key: 'last-name', element: 'text', label: 'Last name' },
587
+ ],
588
+ showCouponCodeField: true,
589
+ formLayout,
590
+ });
591
+ ```
592
+
593
+ Responses and `update` reference fields by `fieldId` (the `FormLayout` type). `update` replaces the
594
+ whole layout, so read `page.formLayout`, change it and send it back. Fields the layout leaves out
595
+ are added after it. A `fieldKey` that matches no field, a field id that is not on the page, or a
596
+ `discount-code` item on a form, throws a `ValidationError`. The `discount-code` item only places
597
+ the box: `showCouponCodeField` switches it on, and a saved placement is returned while the box is
598
+ off too. More item types may be added later: skip any type you do not recognise when reading, and
599
+ send it back as you received it when updating.
600
+
330
601
  ### Webhooks
331
602
 
332
603
  #### List webhook endpoints
@@ -362,8 +633,36 @@ saveSecret(webhook.secret);
362
633
  > and a URL already registered on the account is rejected with a `ConflictError`.
363
634
 
364
635
  > The create response is the **only** place the signing `secret` is ever returned.
365
- > It is not included in `list()` responses and cannot be retrieved later — store it
366
- > when you create the endpoint. Omit `secret` to have one generated for you.
636
+ > It is not included in `list()` or `get()` responses and cannot be retrieved later, so store
637
+ > it when you create the endpoint. Omit `secret` to have one generated for you.
638
+
639
+ #### Get a webhook endpoint
640
+
641
+ ```typescript
642
+ const { data: webhook } = await checkoutpage.webhooks.get(webhookId);
643
+
644
+ console.log(webhook.status, webhook.deliveryCount, webhook.lastFailureAt);
645
+ ```
646
+
647
+ The signing `secret` is not included.
648
+
649
+ #### Update a webhook endpoint
650
+
651
+ ```typescript
652
+ const { data: updated } = await checkoutpage.webhooks.update(webhookId, {
653
+ url: 'https://example.com/hooks/checkoutpage-v2',
654
+ events: ['payment.paid', 'checkout_page.updated', 'product.updated'],
655
+ });
656
+
657
+ // Pause deliveries without deleting the endpoint, then resume them
658
+ await checkoutpage.webhooks.update(webhookId, { status: 'inactive' });
659
+ await checkoutpage.webhooks.update(webhookId, { status: 'active' });
660
+ ```
661
+
662
+ Only the fields you send change. `events` and `customHeaders` replace the existing values rather
663
+ than merging with them. A new `url` must use https and must not already be registered on another of
664
+ your webhooks (`ValidationError` / `ConflictError`). The signing secret cannot be changed through
665
+ the API.
367
666
 
368
667
  #### Delete a webhook endpoint
369
668
 
@@ -373,6 +672,39 @@ const { data: deleted } = await checkoutpage.webhooks.delete(webhook.id);
373
672
 
374
673
  Deliveries stop immediately and the endpoint no longer appears in `list()`.
375
674
 
675
+ ### Test mode
676
+
677
+ Switch a checkout page, event or form into test mode with `testmode: true`. While it is on, every
678
+ purchase on that page is a test purchase: no money is taken, the buyer gets the product for free,
679
+ and the records are created with `livemode: false`.
680
+
681
+ ```typescript
682
+ await checkoutpage.checkoutPages.update(pageId, { testmode: true });
683
+
684
+ const { data: pagesInTestMode } = await checkoutpage.checkoutPages.list({ testmode: true });
685
+
686
+ // Turn it off to take real payments again
687
+ await checkoutpage.checkoutPages.update(pageId, { testmode: false });
688
+ ```
689
+
690
+ `events` and `forms` take the same field on `create`, `update` and `list`. Page responses carry
691
+ `testmode` and `testmodeEnabledAt` (when test mode was switched on, `null` while it is off).
692
+
693
+ #### Read test records
694
+
695
+ Pass `livemode: false` to read test records from payments, bookings, subscriptions, subscription
696
+ payments, invoices, submissions and tickets:
697
+
698
+ ```typescript
699
+ const { data: testPayments } = await checkoutpage.payments.list({ pageId, livemode: false });
700
+ const { data: testPayment } = await checkoutpage.payments.get(paymentId, { livemode: false });
701
+ const pdf = await checkoutpage.bookings.downloadTicketPdf(bookingId, { livemode: false });
702
+ ```
703
+
704
+ Without `livemode`, reads return records in the mode of your API key. A live key (`sk_live_`)
705
+ reads live data by default and test data with `livemode: false`. A test key (`sk_test_`) only
706
+ reads test data: `livemode: true` with a test key throws an `AuthenticationError` (403).
707
+
376
708
  ## Error Handling
377
709
 
378
710
  The SDK provides typed error classes for different error scenarios: