@blockmoblabs/cryptopay 0.1.0 → 0.2.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
@@ -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,7 @@ 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.
37
36
 
38
37
  | Method | What |
39
38
  | --- | --- |
@@ -41,10 +40,39 @@ reports which one you are on.
41
40
  | `getIntent(id)` | Read one payment, including its deposit sessions. |
42
41
  | `listIntents(query)` | Page through payments; `search` matches any of your references. |
43
42
  | `cancelIntent(id)` | Cancel an unpaid payment. Refused once completed. |
43
+ | `createSubscription(input)` | Bill a customer every week, month or year. See below. |
44
+ | `getSubscription(id)` / `listSubscriptions()` | Read subscriptions and their next due date. |
45
+ | `cancelSubscription(id)` | Stop future payments. |
44
46
  | `verifyWebhook(opts)` | Verify and parse a webhook. Throws on anything suspect. |
45
47
 
46
48
  Non-2xx responses throw `CryptoPayError` with `status` and `body`.
47
49
 
50
+ ## Subscriptions
51
+
52
+ Crypto cannot be pulled from a wallet, so a subscription is a recurring invoice. Three days before
53
+ each due date the customer is emailed a payment link, then reminded once a day. If it is still
54
+ unpaid three days after the due date, the subscription lapses.
55
+
56
+ ```ts
57
+ const sub = await cryptopay.createSubscription({
58
+ amountUsd: 20,
59
+ customerEmail: 'user@example.com',
60
+ interval: 'month', // 'week' | 'month' | 'year'
61
+ firstDueAt: '2026-11-02', // later payments stay on the 2nd
62
+ reference: 'sub_221', // comes back as customer.subscriptionRef
63
+ description: 'Pro plan', // shown to the customer in the emails
64
+ });
65
+ ```
66
+
67
+ In your webhook handler:
68
+
69
+ | Event | What to do |
70
+ | --- | --- |
71
+ | `payment.completed` with `customer.subscriptionRef` | A period was paid. Extend access. |
72
+ | `subscription.renewed` | Same moment; `event.subscription.dueAt` is the next due date. |
73
+ | `subscription.payment_due` | An invoice opened. `event.subscription.checkoutUrl` is the link, if you want to send your own message. |
74
+ | `subscription.lapsed` | Unpaid after the grace period. Remove access. |
75
+
48
76
  ## Browser
49
77
 
50
78
  ```ts
@@ -67,7 +95,7 @@ checkout's origin can drive the modal.
67
95
 
68
96
  ## Webhooks
69
97
 
70
- Pass the **raw** body — a re-serialized object will not match the signature.
98
+ Pass the **raw** body: a re-serialized object will not match the signature.
71
99
 
72
100
  ```ts
73
101
  app.post('/webhooks/cryptopay', express.raw({ type: 'application/json' }), async (req, res) => {
@@ -100,18 +128,14 @@ old event does not get through. Delivery is at-least-once: dedupe on `event.id`.
100
128
  Events: `payment.created`, `payment.detected`, `payment.confirming`, `payment.underpaid`,
101
129
  `payment.completed`, `payment.expired`, `payment.failed`.
102
130
 
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.
131
+ ## Trying it out
110
132
 
111
- Going live is one environment variable: the key changes, the code does not.
133
+ Accounts are live from sign-up. Make a small real payment to see the whole lifecycle, or create a
134
+ payment link from the dashboard and open its checkout to see what your customers see. Events can be
135
+ re-sent from Dashboard -> Webhooks while you build your handler.
112
136
 
113
137
  ## Fees
114
138
 
115
- $2 flat per successful payment, charged once on completion. It comes off your side — the payer is
139
+ 1% per successful payment with a $0.50 minimum, charged once on completion. It comes off your side: the payer is
116
140
  asked for exactly the invoice amount. On a $40 payment, `fee` is
117
- `{ grossUsd: 40, feeUsd: 2, netUsd: 38 }`.
141
+ `{ 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
@@ -17,7 +17,7 @@ export class CryptoPay {
17
17
  if (!options.apiKey)
18
18
  throw new Error('CryptoPay: apiKey is required');
19
19
  this.apiKey = options.apiKey;
20
- this.baseUrl = (options.baseUrl ?? 'https://api.cryptopay.blockmob.io').replace(/\/+$/, '');
20
+ this.baseUrl = (options.baseUrl ?? 'https://apicp.blockmob.io').replace(/\/+$/, '');
21
21
  this.timeoutMs = options.timeoutMs ?? 20_000;
22
22
  this.fetchImpl = options.fetch ?? globalThis.fetch;
23
23
  if (!this.fetchImpl)
@@ -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.0",
3
+ "version": "0.2.0",
4
4
  "description": "CryptoPay crypto payment infrastructure: create payment intents, open the checkout, verify webhooks.",
5
5
  "license": "MIT",
6
6
  "type": "module",