@blockmoblabs/cryptopay 0.1.1 → 0.2.1

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
@@ -16,7 +16,7 @@ npm install @blockmoblabs/cryptopay
16
16
  import { CryptoPay } from '@blockmoblabs/cryptopay';
17
17
 
18
18
  const cryptopay = new CryptoPay({
19
- apiKey: process.env.CRYPTOPAY_API_KEY!, // cp_test_… or cp_live_…
19
+ apiKey: process.env.CRYPTOPAY_API_KEY!, // cp_live_… from Dashboard -> API keys
20
20
  baseUrl: process.env.CRYPTOPAY_API_URL, // only when self-hosting
21
21
  });
22
22
 
@@ -32,8 +32,8 @@ const intent = await cryptopay.createIntent({
32
32
  });
33
33
  ```
34
34
 
35
- The key decides the merchant **and** the mode — there is no mode flag to get wrong. `cryptopay.mode`
36
- reports which one you are on.
35
+ The key identifies the merchant; there is no mode flag to get wrong. Leave out `successUrl` or
36
+ `cancelUrl` and the defaults from Dashboard -> Settings are used.
37
37
 
38
38
  | Method | What |
39
39
  | --- | --- |
@@ -41,10 +41,39 @@ reports which one you are on.
41
41
  | `getIntent(id)` | Read one payment, including its deposit sessions. |
42
42
  | `listIntents(query)` | Page through payments; `search` matches any of your references. |
43
43
  | `cancelIntent(id)` | Cancel an unpaid payment. Refused once completed. |
44
+ | `createSubscription(input)` | Bill a customer every week, month or year. See below. |
45
+ | `getSubscription(id)` / `listSubscriptions()` | Read subscriptions and their next due date. |
46
+ | `cancelSubscription(id)` | Stop future payments. |
44
47
  | `verifyWebhook(opts)` | Verify and parse a webhook. Throws on anything suspect. |
45
48
 
46
49
  Non-2xx responses throw `CryptoPayError` with `status` and `body`.
47
50
 
51
+ ## Subscriptions
52
+
53
+ Crypto cannot be pulled from a wallet, so a subscription is a recurring invoice. Three days before
54
+ each due date the customer is emailed a payment link, then reminded once a day. If it is still
55
+ unpaid three days after the due date, the subscription lapses.
56
+
57
+ ```ts
58
+ const sub = await cryptopay.createSubscription({
59
+ amountUsd: 20,
60
+ customerEmail: 'user@example.com',
61
+ interval: 'month', // 'week' | 'month' | 'year'
62
+ firstDueAt: '2026-11-02', // later payments stay on the 2nd
63
+ reference: 'sub_221', // comes back as customer.subscriptionRef
64
+ description: 'Pro plan', // shown to the customer in the emails
65
+ });
66
+ ```
67
+
68
+ In your webhook handler:
69
+
70
+ | Event | What to do |
71
+ | --- | --- |
72
+ | `payment.completed` with `customer.subscriptionRef` | A period was paid. Extend access. |
73
+ | `subscription.renewed` | Same moment; `event.subscription.dueAt` is the next due date. |
74
+ | `subscription.payment_due` | An invoice opened. `event.subscription.checkoutUrl` is the link, if you want to send your own message. |
75
+ | `subscription.lapsed` | Unpaid after the grace period. Remove access. |
76
+
48
77
  ## Browser
49
78
 
50
79
  ```ts
@@ -67,7 +96,7 @@ checkout's origin can drive the modal.
67
96
 
68
97
  ## Webhooks
69
98
 
70
- Pass the **raw** body — a re-serialized object will not match the signature.
99
+ Pass the **raw** body: a re-serialized object will not match the signature.
71
100
 
72
101
  ```ts
73
102
  app.post('/webhooks/cryptopay', express.raw({ type: 'application/json' }), async (req, res) => {
@@ -100,18 +129,14 @@ old event does not get through. Delivery is at-least-once: dedupe on `event.id`.
100
129
  Events: `payment.created`, `payment.detected`, `payment.confirming`, `payment.underpaid`,
101
130
  `payment.completed`, `payment.expired`, `payment.failed`.
102
131
 
103
- ## Test mode
104
-
105
- A test key runs the whole lifecycle against a simulator — real intents, statuses and webhooks, no
106
- chain and no money. The deposit address is `cptest_…`, deliberately invalid on every network, and the
107
- payment confirms on a timer: detected within ~15s and confirmed within ~30s, since the watcher that
108
- advances it ticks every 15 seconds. In an automated test, call `POST /sessions/:id/recheck` (what *I
109
- have paid* does in the checkout) to confirm immediately instead of sleeping.
132
+ ## Trying it out
110
133
 
111
- Going live is one environment variable: the key changes, the code does not.
134
+ Accounts are live from sign-up. Make a small real payment to see the whole lifecycle, or create a
135
+ payment link from the dashboard and open its checkout to see what your customers see. Events can be
136
+ re-sent from Dashboard -> Webhooks while you build your handler.
112
137
 
113
138
  ## Fees
114
139
 
115
- $2 flat per successful payment, charged once on completion. It comes off your side — the payer is
140
+ 1% per successful payment with a $0.50 minimum, charged once on completion. It comes off your side: the payer is
116
141
  asked for exactly the invoice amount. On a $40 payment, `fee` is
117
- `{ grossUsd: 40, feeUsd: 2, netUsd: 38 }`.
142
+ `{ grossUsd: 40, feeUsd: 0.5, netUsd: 39.5 }`.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type CreateIntentInput, type PaymentIntent, type WebhookEvent } from './types.js';
1
+ import { type CreateIntentInput, type CreateSubscriptionInput, type PaymentIntent, type Subscription, type WebhookEvent } from './types.js';
2
2
  export * from './types.js';
3
3
  export type CryptoPayOptions = {
4
4
  /** `cp_test_…` or `cp_live_…`. The key decides the mode; there is no mode flag to get wrong. */
@@ -41,6 +41,18 @@ export declare class CryptoPay {
41
41
  intents: PaymentIntent[];
42
42
  nextCursor: string | null;
43
43
  }>;
44
+ /**
45
+ * Bill a customer on a schedule. Three days before each due date they are emailed a payment link,
46
+ * then reminded daily; unpaid three days after it, the subscription lapses. Each payment arrives
47
+ * as an ordinary `payment.completed` webhook carrying your `reference` as `subscriptionRef`.
48
+ */
49
+ createSubscription(input: CreateSubscriptionInput): Promise<Subscription>;
50
+ getSubscription(id: string): Promise<Subscription>;
51
+ listSubscriptions(): Promise<{
52
+ subscriptions: Subscription[];
53
+ }>;
54
+ /** Stops future payments. Safe to call twice. */
55
+ cancelSubscription(id: string): Promise<Subscription>;
44
56
  /**
45
57
  * Verify a webhook and return its parsed body. Pass the RAW request body — a re-serialized object
46
58
  * will not match the signature. Throws if the signature, the headers, or the timestamp is wrong,
package/dist/index.js CHANGED
@@ -53,6 +53,24 @@ export class CryptoPay {
53
53
  const suffix = params.toString();
54
54
  return this.request('GET', `/payment-intents${suffix ? `?${suffix}` : ''}`);
55
55
  }
56
+ /**
57
+ * Bill a customer on a schedule. Three days before each due date they are emailed a payment link,
58
+ * then reminded daily; unpaid three days after it, the subscription lapses. Each payment arrives
59
+ * as an ordinary `payment.completed` webhook carrying your `reference` as `subscriptionRef`.
60
+ */
61
+ createSubscription(input) {
62
+ return this.request('POST', '/subscriptions', { body: input });
63
+ }
64
+ getSubscription(id) {
65
+ return this.request('GET', `/subscriptions/${encodeURIComponent(id)}`);
66
+ }
67
+ listSubscriptions() {
68
+ return this.request('GET', '/subscriptions');
69
+ }
70
+ /** Stops future payments. Safe to call twice. */
71
+ cancelSubscription(id) {
72
+ return this.request('POST', `/subscriptions/${encodeURIComponent(id)}/cancel`);
73
+ }
56
74
  /**
57
75
  * Verify a webhook and return its parsed body. Pass the RAW request body — a re-serialized object
58
76
  * will not match the signature. Throws if the signature, the headers, or the timestamp is wrong,
package/dist/types.d.ts CHANGED
@@ -76,7 +76,41 @@ export type CreateIntentInput = {
76
76
  /** Repeating a request with the same key returns the original intent instead of charging twice. */
77
77
  idempotencyKey?: string;
78
78
  };
79
- export type WebhookEventType = 'payment.created' | 'payment.detected' | 'payment.confirming' | 'payment.underpaid' | 'payment.completed' | 'payment.expired' | 'payment.failed' | 'sweep.failed';
79
+ export type WebhookEventType = 'payment.created' | 'payment.detected' | 'payment.confirming' | 'payment.underpaid' | 'payment.completed' | 'payment.expired' | 'payment.failed' | 'sweep.failed' | 'subscription.payment_due' | 'subscription.renewed' | 'subscription.lapsed';
80
+ export type CreateSubscriptionInput = {
81
+ /** Charged every interval. 0.50 to 1,000,000, at most 2 decimals. */
82
+ amountUsd: number;
83
+ /** Who is billed. Payment links and reminders are emailed here. */
84
+ customerEmail: string;
85
+ interval: 'week' | 'month' | 'year';
86
+ /** When the first payment is due (ISO date). Left out: now. Later ones keep the same day. */
87
+ firstDueAt?: string;
88
+ customerRef?: string;
89
+ /** Your own id for this subscription. Comes back as `customer.subscriptionRef` on every payment. */
90
+ reference?: string;
91
+ /** What is being paid for, as the customer reads it in the emails. */
92
+ description?: string;
93
+ metadata?: Record<string, unknown>;
94
+ };
95
+ export type Subscription = {
96
+ id: string;
97
+ mode: ApiMode;
98
+ status: 'active' | 'lapsed' | 'cancelled';
99
+ customerEmail: string;
100
+ customerRef: string | null;
101
+ reference: string | null;
102
+ description: string | null;
103
+ amountUsd: number;
104
+ interval: 'week' | 'month' | 'year';
105
+ /** When the period now being collected is due. Null once the subscription has ended. */
106
+ dueAt: string | null;
107
+ periodsPaid: number;
108
+ /** The open invoice (a payment intent), once the customer has been asked to pay it. */
109
+ intentId: string | null;
110
+ metadata: unknown;
111
+ endedAt: string | null;
112
+ createdAt: string;
113
+ };
80
114
  export type WebhookEvent = {
81
115
  id: string;
82
116
  type: WebhookEventType;
@@ -99,6 +133,10 @@ export type WebhookEvent = {
99
133
  feeUsd: number;
100
134
  netUsd: number;
101
135
  } | null;
136
+ /** On `subscription.*` events. `checkoutUrl` is set on `subscription.payment_due`. */
137
+ subscription?: Subscription & {
138
+ checkoutUrl?: string | null;
139
+ };
102
140
  payment: {
103
141
  sessionId: string;
104
142
  chain: Chain;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockmoblabs/cryptopay",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "CryptoPay crypto payment infrastructure: create payment intents, open the checkout, verify webhooks.",
5
5
  "license": "MIT",
6
6
  "type": "module",