@klappay/types 1.0.0 → 1.0.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/CHANGELOG.md +25 -0
- package/README.md +5 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/docs/auth-and-accounts.md +188 -0
- package/docs/charges.md +303 -0
- package/docs/distributions.md +93 -0
- package/docs/errors-and-health.md +52 -0
- package/docs/getting-started.md +96 -0
- package/docs/sandbox.md +80 -0
- package/docs/tokens-and-networks.md +108 -0
- package/docs/webhooks.md +165 -0
- package/package.json +13 -7
- package/logo.png +0 -0
package/docs/charges.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Charges
|
|
2
|
+
|
|
3
|
+
The core resource. Everything here is exported from `charges.ts` unless
|
|
4
|
+
noted otherwise.
|
|
5
|
+
|
|
6
|
+
## Creating a charge
|
|
7
|
+
|
|
8
|
+
`CreateChargeSchema` / `CreateChargeInput` — the body of `POST
|
|
9
|
+
/v1/charges`:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { CreateChargeSchema, type CreateChargeInput } from '@klappay/types'
|
|
13
|
+
|
|
14
|
+
const input: CreateChargeInput = {
|
|
15
|
+
amount: 49.9,
|
|
16
|
+
acceptedPayments: [
|
|
17
|
+
{ token: 'USDC', network: 'base' },
|
|
18
|
+
{ token: 'USDC', network: 'optimism' },
|
|
19
|
+
{ token: 'USDT', network: 'base' },
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
CreateChargeSchema.parse(input) // throws on anything invalid
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Fields worth knowing about:
|
|
27
|
+
|
|
28
|
+
- `mode` — optional, defaults to `standard`. `standard` is the usual
|
|
29
|
+
lifecycle: accumulates transfers toward one resolution
|
|
30
|
+
(`confirmed`/`expired`/`underpaid`), settles once.
|
|
31
|
+
`continuous` never resolves — `status` stays `pending` for the
|
|
32
|
+
charge's entire life, and every credited transfer settles
|
|
33
|
+
independently instead of accumulating toward one confirmation (see
|
|
34
|
+
`charge.contribution_received`/`charge.contribution_settled` below).
|
|
35
|
+
`mode: 'continuous'` requires both `amount` and `expiresIn` to be
|
|
36
|
+
omitted — rejected with `400 validation_error` otherwise — and is
|
|
37
|
+
never inferred just from omitting those two fields, a deliberate
|
|
38
|
+
choice so a charge's entire settlement lifecycle never silently
|
|
39
|
+
flips based on which optional fields happened to be left out.
|
|
40
|
+
- `amount` — optional. Omit it entirely for a "pay what you want" charge
|
|
41
|
+
that accepts any positive amount — the first credited transfer of any
|
|
42
|
+
size confirms it (`ChargeSchema.amount` reads back `null`), and
|
|
43
|
+
`isOverpaid` never applies since there's no target to exceed. Combines
|
|
44
|
+
freely with `expiresIn`: keep it for the usual checkout amount, omit
|
|
45
|
+
both `amount` and `expiresIn` together for a `standard`-mode charge
|
|
46
|
+
with neither, or for a `continuous`-mode charge (required for that
|
|
47
|
+
mode — see `mode` above). When a value *is* given, the usual bounds
|
|
48
|
+
still apply (`.positive()`, up to `CHARGE_AMOUNT_MAX`).
|
|
49
|
+
- `acceptedPayments` — `AcceptedPaymentSchema[]` (`{ token: Token,
|
|
50
|
+
network: Network }`), at least one entry, up to
|
|
51
|
+
`CHARGE_ACCEPTED_PAYMENTS_MAX` (`14` — 2 tokens × 7 operational
|
|
52
|
+
networks, the actual ceiling of distinct pairs that can exist today,
|
|
53
|
+
not an arbitrary round number). The payer can pay with *any* pair in
|
|
54
|
+
this list — the on-chain address is identical no matter which one they
|
|
55
|
+
use (0xSplits addresses are chain-agnostic), so offering more than one
|
|
56
|
+
is just a matter of listing more pairs, not generating more addresses.
|
|
57
|
+
Duplicate pairs in the array are rejected with `400 validation_error`
|
|
58
|
+
at the schema level. Each pair's `network` must be in
|
|
59
|
+
`OPERATIONAL_NETWORKS`, and the exact `(token, network)` combination
|
|
60
|
+
must have a real entry in `TOKEN_ADDRESSES` for your key's
|
|
61
|
+
`environment` — an unconfigured pair anywhere in the array rejects the
|
|
62
|
+
*whole* request with `422 token_not_supported`, naming the first bad
|
|
63
|
+
index (e.g. `param: "acceptedPayments[1]"`), not silently dropped. Not
|
|
64
|
+
every token is deployed on every `network`/`environment` — `USDT` has
|
|
65
|
+
no `test` deployment on `base`, `optimism`, or `ethereum` (Tether
|
|
66
|
+
doesn't issue an official Sepolia one, unlike Circle's `USDC`);
|
|
67
|
+
`arbitrum`, `polygon`, and `avalanche` have no `test` environment at
|
|
68
|
+
all yet, for either token (0xSplits itself has no Arbitrum Sepolia
|
|
69
|
+
deployment and no Polygon or Avalanche Fuji testnet support); `bnb`
|
|
70
|
+
has no `test` environment either, and its `USDC` (unlike every other
|
|
71
|
+
`TOKEN_ADDRESSES` entry) is Binance-Peg, not Circle-issued — see
|
|
72
|
+
`packages/types/docs/tokens-and-networks.md` for the full risk note.
|
|
73
|
+
Call `GET /v1/networks` to get the live matrix for your environment
|
|
74
|
+
instead of hardcoding it client-side — it reads the exact same
|
|
75
|
+
`TOKEN_ADDRESSES` lookup this validation does, so it can never list a
|
|
76
|
+
pair charge creation would then reject.
|
|
77
|
+
- `currency` — always `"USD"` today (`z.literal('USD')`), the only
|
|
78
|
+
supported value. Present as a field (not hardcoded) so a second
|
|
79
|
+
currency can be added later without a breaking shape change.
|
|
80
|
+
- `expiresIn` — seconds the charge stays open, `CHARGE_EXPIRES_IN_MIN_SECONDS`
|
|
81
|
+
(60) to `CHARGE_EXPIRES_IN_MAX_SECONDS` (365 days). Omit it entirely for
|
|
82
|
+
a charge that never expires — `expiresAt` is `null` and it stays
|
|
83
|
+
`PENDING`/`PARTIALLY_PAID` indefinitely, useful for donations,
|
|
84
|
+
investments, or anything with no natural deadline. Cannot be extended
|
|
85
|
+
or shortened after creation either way.
|
|
86
|
+
- `idempotencyKey` — scoped to your organization. Replaying the same key
|
|
87
|
+
**with the exact same request body** returns the *original* charge
|
|
88
|
+
unchanged instead of creating a duplicate — the safe way to retry a
|
|
89
|
+
request after a timeout. Reusing a key with a *different* body (a
|
|
90
|
+
different `amount`, say) is a `409 idempotency_key_reused` error, not a
|
|
91
|
+
silent return of the original charge — an idempotency key means "retry
|
|
92
|
+
this exact request," not "look up a charge by this string."
|
|
93
|
+
- `externalRef` — an opaque id from your own system (e.g. an order id),
|
|
94
|
+
echoed back on the charge and every webhook payload. Not interpreted
|
|
95
|
+
by Klap.
|
|
96
|
+
- `source` — a free-form label for what created this charge (e.g.
|
|
97
|
+
`"checkout"`, `"invoice"`) if you create charges from more than one
|
|
98
|
+
flow. Not a fixed enum on purpose — use whatever values make sense to
|
|
99
|
+
you.
|
|
100
|
+
- `metadata` — arbitrary key/value data, returned as-is on every read.
|
|
101
|
+
|
|
102
|
+
`CHARGE_AMOUNT_MAX` (`999_999_999_999`) is exported too — it's the real
|
|
103
|
+
ceiling `amount` is validated against, matching the `Decimal(18, 6)`
|
|
104
|
+
column charges are stored in. That column also caps precision at 6
|
|
105
|
+
decimal places — `amount` and, on reads, `amountReceived` support up to
|
|
106
|
+
6 fractional digits; anything more precise is silently truncated.
|
|
107
|
+
|
|
108
|
+
## The `Charge` shape
|
|
109
|
+
|
|
110
|
+
`ChargeSchema` / `Charge` is what every read (`GET /v1/charges/{id}`,
|
|
111
|
+
`GET /v1/charges`, and every webhook's `data` for a `charge.*` event)
|
|
112
|
+
returns. A few fields that aren't self-explanatory:
|
|
113
|
+
|
|
114
|
+
- `mode` (`ChargeModeSchema`) — `standard` or `continuous`, set at
|
|
115
|
+
creation, never changes. See `docs/payments.md`'s "Continuous mode"
|
|
116
|
+
for the full lifecycle difference — the short version: a
|
|
117
|
+
`continuous` charge's `status` never leaves `pending`, and it settles
|
|
118
|
+
via `charge.contribution_received`/`charge.contribution_settled`
|
|
119
|
+
instead of the usual `charge.confirmed`/`charge.settled` pair.
|
|
120
|
+
- `amount` — the requested target, or `null` if the charge accepts any
|
|
121
|
+
amount (`amount` was omitted at creation, or `mode: 'continuous'`,
|
|
122
|
+
which requires it). Never changes after creation.
|
|
123
|
+
- `acceptedPayments` — echoes exactly what was configured at creation;
|
|
124
|
+
never changes afterward, regardless of which pair actually ends up
|
|
125
|
+
paid.
|
|
126
|
+
- `paidWith` (`AcceptedPayment[]`) — every distinct `(token, network)`
|
|
127
|
+
pair that has actually contributed a credited transfer so far. Empty
|
|
128
|
+
until the first transfer arrives — not `null`; a charge that's never
|
|
129
|
+
been paid returns `paidWith: []`. Every accepted pair sums: a charge
|
|
130
|
+
configured with `acceptedPayments: [USDC/base, USDT/base]` can be
|
|
131
|
+
confirmed by $9 in USDC plus $1 in USDT, and `paidWith` would then hold
|
|
132
|
+
both pairs. Only a transfer on a pair that's actually in
|
|
133
|
+
`acceptedPayments` is credited — one on any other pair is still
|
|
134
|
+
recorded (visible in the charge's timeline) but never counted toward
|
|
135
|
+
`amountReceived`.
|
|
136
|
+
- `amountReceived` — cumulative amount actually received on-chain so
|
|
137
|
+
far. `null` until the first transfer arrives. Can exceed `amount` —
|
|
138
|
+
see `isOverpaid` — unless `amount` itself is `null`, in which case
|
|
139
|
+
`isOverpaid` never applies (there's no target to exceed).
|
|
140
|
+
- `isOverpaid` — `true` once `amountReceived` ends up greater than
|
|
141
|
+
`amount`; always `false` for a charge with no `amount` at all. Klap
|
|
142
|
+
never auto-refunds the difference — the charge address is an immutable
|
|
143
|
+
split with the payer never a recipient, and Klap never custodies funds
|
|
144
|
+
to refund from — this field is how you detect an overpayment happened
|
|
145
|
+
so you can decide what to do about it yourself.
|
|
146
|
+
- `status` (`ChargeStatusSchema`) — payment progress from the payer's
|
|
147
|
+
side only: `pending` → `partially_paid`/`confirmed` → (if it never
|
|
148
|
+
fully pays) `expired`/`underpaid`. Never reflects whether the
|
|
149
|
+
*merchant* actually got paid — that's `settlementStatus`.
|
|
150
|
+
- `settlementStatus` (`SettlementStatusSchema`, nullable) — a
|
|
151
|
+
**separate** step from `status`. `status: confirmed` only means the
|
|
152
|
+
payment was detected on-chain; `settlementStatus: completed` means the
|
|
153
|
+
merchant's wallet actually has the funds. `null` on the parent
|
|
154
|
+
`Charge` means no payout has been attempted yet.
|
|
155
|
+
- `settledAt` (nullable) — when `settlementStatus` first reached
|
|
156
|
+
`completed`, mirroring `confirmedAt`'s role for `status`. `null` while
|
|
157
|
+
`settlementStatus` is `pending`/`failed`/`null`.
|
|
158
|
+
- `environment` (`EnvironmentSchema`) — `live` or `test`, matching the
|
|
159
|
+
API key used to create it. `live` settles on Base mainnet; `test`
|
|
160
|
+
settles on Base Sepolia, a real testnet — real on-chain activity, just
|
|
161
|
+
never real money.
|
|
162
|
+
- `address` — the on-chain address the payer sends funds to, identical
|
|
163
|
+
no matter which `acceptedPayments` pair they use (0xSplits addresses
|
|
164
|
+
are chain-agnostic — CREATE2 with no `chainId` in the derivation).
|
|
165
|
+
Unique per charge, predicted at creation — funds sent here go directly
|
|
166
|
+
to the merchant; Klap never custodies them.
|
|
167
|
+
- `lastActivityAt` — when a transfer was last credited toward this
|
|
168
|
+
charge, or `createdAt` if none has arrived yet. Only meaningful for a
|
|
169
|
+
charge with no `expiresAt` — see `pausedAt`.
|
|
170
|
+
- `pausedAt` (nullable) — only ever set for a charge with no `expiresAt`.
|
|
171
|
+
`null` means Klap is actively watching the address in real time (the
|
|
172
|
+
normal case). A timestamp means no contribution arrived for longer
|
|
173
|
+
than the inactivity window (90 days with a goal `amount`, 365 without)
|
|
174
|
+
and real-time watching was stopped — the charge itself is never
|
|
175
|
+
closed, a transfer can still land and be credited, just detected on a
|
|
176
|
+
much slower fallback poll instead of instantly. Clears automatically
|
|
177
|
+
(watching resumes) the moment that happens — see `charge.paused`/
|
|
178
|
+
`charge.reactivated` in `webhooks.md`.
|
|
179
|
+
|
|
180
|
+
## Listing charges
|
|
181
|
+
|
|
182
|
+
`ListChargesSchema` / `ListChargesInput` — the query params for `GET
|
|
183
|
+
/v1/charges`. Supports filtering by `status`/`environment`/`isOverpaid`,
|
|
184
|
+
and `since` (filters on `createdAt`, not on when the status last
|
|
185
|
+
changed — matters if you're polling as a fallback for missed webhooks:
|
|
186
|
+
use a window at least as wide as your longest `expiresIn`, or a
|
|
187
|
+
long-lived charge that changed status outside a narrower window gets
|
|
188
|
+
missed; a never-expiring charge (`expiresIn` omitted) can change status
|
|
189
|
+
arbitrarily far in the future, so no fixed `since` window fully covers
|
|
190
|
+
it — pair polling with `GET /v1/charges/{id}/events` for those instead
|
|
191
|
+
of relying on `since` alone). `token`/`network` filters are also
|
|
192
|
+
supported, but filter on
|
|
193
|
+
`paidWith` — "charges actually paid in X" — not on `acceptedPayments`;
|
|
194
|
+
filtering by "charges that accept X" isn't supported yet. Cursor-paginated
|
|
195
|
+
via `limit` (1–100, default 20) and `cursor`.
|
|
196
|
+
|
|
197
|
+
`PaginatedChargesSchema` / `PaginatedCharges` is the response shape:
|
|
198
|
+
`{ data: Charge[], nextCursor: string | null, hasMore: boolean }`.
|
|
199
|
+
|
|
200
|
+
### The shared cursor pagination pattern
|
|
201
|
+
|
|
202
|
+
Every list endpoint in the API (`GET /v1/charges`, `GET /v1/organizations`,
|
|
203
|
+
`GET /v1/organizations/{id}/api-keys`, `GET /v1/organizations/{id}/users`,
|
|
204
|
+
`GET /v1/webhooks/{id}/deliveries`) shares this exact
|
|
205
|
+
same `{ limit, cursor }` → `{ data, nextCursor, hasMore }` shape, built
|
|
206
|
+
from one pair of generic helpers in `pagination.ts`:
|
|
207
|
+
`PaginationQuerySchema` (the `{ limit, cursor }` query params) and
|
|
208
|
+
`paginatedSchema(itemSchema)` (wraps any item schema into the response
|
|
209
|
+
envelope) — `ListChargesSchema`/`PaginatedChargesSchema` above are just
|
|
210
|
+
`PaginationQuerySchema`/`paginatedSchema(ChargeSchema)` composed with
|
|
211
|
+
charge-specific filters, and `ListApiKeysSchema`/`ListUsersSchema`/
|
|
212
|
+
`ListWebhookDeliveriesSchema` (and their `Paginated*Schema` response
|
|
213
|
+
counterparts, in `api-keys.ts`/`users.ts`/`webhooks.ts`) follow the same
|
|
214
|
+
pattern with no filters of their own.
|
|
215
|
+
|
|
216
|
+
`cursor` is always opaque — never construct or parse one yourself; pass
|
|
217
|
+
the previous response's `nextCursor` back verbatim to get the next page,
|
|
218
|
+
and stop once `hasMore` is `false` (`nextCursor` is `null` at that
|
|
219
|
+
point). `GET /v1/webhooks` and `GET /v1/charges/{id}/timeline`
|
|
220
|
+
deliberately stay unpaginated — the former is hard-capped at 20 active
|
|
221
|
+
webhooks per organization, the latter is scoped to one charge's own
|
|
222
|
+
events, both naturally small.
|
|
223
|
+
|
|
224
|
+
## Live status without polling
|
|
225
|
+
|
|
226
|
+
`ChargeStatusEventSchema` / `ChargeStatusEvent` is the **minimal**
|
|
227
|
+
payload streamed by the public, unauthenticated
|
|
228
|
+
`GET /v1/verify/{id}/events` SSE endpoint — `id`/`status`/
|
|
229
|
+
`settlementStatus`/`amount`/`amountReceived`/`paidWith`, no
|
|
230
|
+
`acceptedPayments`, fee-split, or address details. The authenticated
|
|
231
|
+
`GET /v1/charges/{id}/events` endpoint streams the full `Charge` instead
|
|
232
|
+
(no separate schema needed — it's the same `ChargeSchema` above).
|
|
233
|
+
|
|
234
|
+
## Proof of payment — `verify.ts`
|
|
235
|
+
|
|
236
|
+
`VerifyChargeSchema` / `VerifyCharge` is the response shape of the
|
|
237
|
+
public `GET /v1/verify/{id}` endpoint — proof-of-payment for anyone
|
|
238
|
+
holding a charge id, no API key required. Serves a `standard`-mode
|
|
239
|
+
charge only once `status` reaches `confirmed`; a `mode: 'continuous'`
|
|
240
|
+
charge is served as soon as it has received at least one contribution,
|
|
241
|
+
since it never reaches `confirmed` at all — every other case (unknown
|
|
242
|
+
id, a `standard` charge still pending, test-mode) returns an identical
|
|
243
|
+
generic `404`. The charge-level fields
|
|
244
|
+
(`id`/`amount`/`amountReceived`/`confirmedAt`/`splitAddress` — `amount`
|
|
245
|
+
is `null` the same way it is on `Charge`, for a charge that accepted
|
|
246
|
+
any amount; `confirmedAt` is `null` for a `continuous` charge, which
|
|
247
|
+
never has a single confirmed moment — use each `payments[]` entry's own
|
|
248
|
+
`settledAt` for per-contribution timing instead) sit alongside `payments`
|
|
249
|
+
(`VerifyPaymentSchema[]`) — **one
|
|
250
|
+
entry per contributing `(token, network)` pair**, since a charge accepting more
|
|
251
|
+
than one pair can now be confirmed by a combination of them (e.g. $9 in
|
|
252
|
+
USDC plus $1 in USDT), and each pair settles independently. Each
|
|
253
|
+
`VerifyPaymentSchema` entry carries its own `token`/`network`/
|
|
254
|
+
`amountReceived`/`txHash`/`explorerTxUrl`, its own exact fee-split
|
|
255
|
+
breakdown (`split`: `VerifySplitEntrySchema[]`, `role`/`address`/
|
|
256
|
+
`percentAllocation`/`amountUSD` per recipient — `role` is `'merchant' |
|
|
257
|
+
'klap_fee' | 'distributor_incentive'`, `SplitRecipientRoleSchema`), and
|
|
258
|
+
its own `splitTxHash`/`settledAt` (both `null` until that specific
|
|
259
|
+
pair's settlement happens — one pair can be settled while another is
|
|
260
|
+
still pending).
|
|
261
|
+
|
|
262
|
+
## Audit trail — `timeline.ts`
|
|
263
|
+
|
|
264
|
+
`TimelineEventSchema` / `TimelineEvent` is the shape of each entry in a
|
|
265
|
+
charge's timeline (`GET /v1/charges/{id}/timeline`) — a read-only,
|
|
266
|
+
assembled-at-request-time view, not a separately stored table. `type`
|
|
267
|
+
(`TimelineEventTypeSchema`) is one of `charge.created`/`charge.expired`/
|
|
268
|
+
`transaction.detected`/`split.distributed`/`webhook.dispatched`/
|
|
269
|
+
`webhook.delivered`/`webhook.failed`; which other fields are present
|
|
270
|
+
depends on `type` (e.g. `txHash`/`amount`/`source`/`token`/`network`/
|
|
271
|
+
`causedTransition` only apply to `transaction.detected`, and `token`/
|
|
272
|
+
`network` are also present on `split.distributed`).
|
|
273
|
+
`TransactionSourceSchema` / `TransactionSource` (`'moralis_webhook' |
|
|
274
|
+
'reconciliation_job' | 'sandbox'`) says *how* a transfer was detected.
|
|
275
|
+
`token`/`network` on a `transaction.detected` event show which
|
|
276
|
+
`acceptedPayments` pair that specific transfer used — useful since a
|
|
277
|
+
charge can accept more than one pair, and every transfer on an accepted
|
|
278
|
+
pair is credited (`causedTransition: true` marks specifically the
|
|
279
|
+
transfer that flipped the charge's `status`, not every credited one — a
|
|
280
|
+
charge paid in installments across two pairs has more than one credited
|
|
281
|
+
`transaction.detected` event with `causedTransition: false`). A
|
|
282
|
+
`split.distributed` event's own `token`/`network` say which pair that
|
|
283
|
+
particular settlement was for — a charge settled across more than one
|
|
284
|
+
pair emits one `split.distributed` event per pair, each independently.
|
|
285
|
+
|
|
286
|
+
## Discovering what you can accept — `capabilities.ts`
|
|
287
|
+
|
|
288
|
+
`CapabilitiesSchema` / `Capabilities` (`{ acceptedPayments:
|
|
289
|
+
AcceptedPayment[] }`) is the response shape of `GET /v1/networks` — the
|
|
290
|
+
live `(token, network)` matrix your API key's `environment` can accept
|
|
291
|
+
right now. It's read from the exact same `TOKEN_ADDRESSES` lookup
|
|
292
|
+
`POST /v1/charges` validates `acceptedPayments` against, so a pair this
|
|
293
|
+
endpoint lists is always safe to submit, and one it doesn't list would
|
|
294
|
+
always be rejected. Use it to build a payment-method picker instead of
|
|
295
|
+
hardcoding the matrix client-side — it changes as new networks/tokens
|
|
296
|
+
come online, with no code change needed on your end to pick that up.
|
|
297
|
+
|
|
298
|
+
## See also
|
|
299
|
+
|
|
300
|
+
- [`webhooks.md`](./webhooks.md) — the full `charge.*` event vocabulary
|
|
301
|
+
and the `WebhookPayload` envelope these events arrive in.
|
|
302
|
+
- [`sandbox.md`](./sandbox.md) — triggering any charge state
|
|
303
|
+
transition without a real on-chain transfer.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Distributions
|
|
2
|
+
|
|
3
|
+
Exported from `distributions.ts`. These back `GET
|
|
4
|
+
/v1/distributions/pending` and `GET /v1/distributions/pending/events` —
|
|
5
|
+
the keeper feed for discovering splits with a settlement payout
|
|
6
|
+
currently claimable via 0xSplits' permissionless `distribute()`. Not a
|
|
7
|
+
typical merchant integration surface — this is for a bot/script calling
|
|
8
|
+
`distribute()` directly to earn the (small) `distributorFeePercent`
|
|
9
|
+
incentive. See `docs/payments.md`'s "Discovering a pending distribution"
|
|
10
|
+
for the full design.
|
|
11
|
+
|
|
12
|
+
## `PendingDistributionSchema`
|
|
13
|
+
|
|
14
|
+
One entry in the array `GET /v1/distributions/pending` returns:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { PendingDistributionSchema, type PendingDistribution } from '@klappay/types'
|
|
18
|
+
|
|
19
|
+
const distribution: PendingDistribution = PendingDistributionSchema.parse({
|
|
20
|
+
splitAddress: '0x727cddb8a015f76c4e8a5c566c7e911761e268fe',
|
|
21
|
+
network: 'base',
|
|
22
|
+
token: 'USDC',
|
|
23
|
+
recipients: [
|
|
24
|
+
{ address: '0x6EadA5173e0ad685455cEFbe4B3df023c0A598A8', percentAllocation: 99.0991 },
|
|
25
|
+
{ address: '0x44Ef725cA64CDAf48fEdb2AED2b156ad32537d41', percentAllocation: 0.9009 },
|
|
26
|
+
],
|
|
27
|
+
distributorFeePercent: 0.1,
|
|
28
|
+
estimatedRewardAmount: 0.001,
|
|
29
|
+
availableSince: '2026-07-28T23:45:52.382Z',
|
|
30
|
+
graceEndsAt: '2026-07-28T23:50:52.382Z',
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`recipients` is always the exact array to pass to `distribute()` —
|
|
35
|
+
0xSplits only stores a hash of the recipient config on-chain, so a
|
|
36
|
+
caller must supply the identical array to prove it matches; there's no
|
|
37
|
+
partial/reconstructed version. `estimatedRewardAmount` is an estimate
|
|
38
|
+
from the amount Klap detected on-chain, not a live balance read — always
|
|
39
|
+
read the split's real balance yourself before submitting a transaction,
|
|
40
|
+
the same way Klap's own settlement worker does. `graceEndsAt` is when
|
|
41
|
+
Klap's own worker may claim it; calling `distribute()` after that point
|
|
42
|
+
is still possible but increasingly likely to lose the race.
|
|
43
|
+
|
|
44
|
+
## `PendingDistributionEventSchema`
|
|
45
|
+
|
|
46
|
+
The shape of each Server-Sent Event's `data:` line from `GET
|
|
47
|
+
/v1/distributions/pending/events` — a discriminated union on `type`:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { PendingDistributionEventSchema, type PendingDistributionEvent } from '@klappay/types'
|
|
51
|
+
|
|
52
|
+
const event: PendingDistributionEvent = PendingDistributionEventSchema.parse(
|
|
53
|
+
JSON.parse(sseEventData),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if (event.type === 'distribution.available') {
|
|
57
|
+
tryClaim(event.distribution) // PendingDistribution, fully typed
|
|
58
|
+
} else {
|
|
59
|
+
cancelPendingAttempt(event.splitAddress) // already claimed by someone else, or by Klap
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`distribution.claimed` fires the moment Klap's own worker claims a row
|
|
64
|
+
too — before it has actually finished calling `distribute()` — not only
|
|
65
|
+
once settlement completes, so a keeper stops racing an attempt already
|
|
66
|
+
in flight. If that attempt then fails and is scheduled for retry,
|
|
67
|
+
`distribution.available` fires again with a refreshed `graceEndsAt`.
|
|
68
|
+
|
|
69
|
+
The stream sends no initial snapshot — open it *before* calling `GET
|
|
70
|
+
/v1/distributions/pending`, then treat both the snapshot response and
|
|
71
|
+
every event received (before or after that call resolves) as an
|
|
72
|
+
idempotent add/remove against one local `Map<splitAddress, PendingDistribution>`.
|
|
73
|
+
Opening the stream *after* the snapshot call instead leaves a real, if
|
|
74
|
+
small, gap where a delta between the two requests is never delivered.
|
|
75
|
+
|
|
76
|
+
## Authentication and environment scoping
|
|
77
|
+
|
|
78
|
+
Both endpoints require the same API key used everywhere else
|
|
79
|
+
(`Authorization: Bearer klap_live_...`/`klap_test_...`) — unlike `GET
|
|
80
|
+
/v1/verify/{id}`, this isn't public. A `test` key only ever sees `test`
|
|
81
|
+
distributions, `live` only `live` — there is no `environment` field on
|
|
82
|
+
either schema above, since the connection is already scoped by whichever
|
|
83
|
+
key opened it.
|
|
84
|
+
|
|
85
|
+
## See also
|
|
86
|
+
|
|
87
|
+
- `docs/payments.md`'s "Discovering a pending distribution" — the full
|
|
88
|
+
design writeup: why authentication is required, why it's still not a
|
|
89
|
+
new on-chain capability, data minimization, and the three points that
|
|
90
|
+
publish `distribution.claimed`.
|
|
91
|
+
- [`charges.md`](./charges.md) — `VerifyCharge`'s `split` array is the
|
|
92
|
+
same kind of recipient/percentage data as `PendingDistribution.recipients`,
|
|
93
|
+
just scoped to one charge instead of every currently-open grace period.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Errors and health
|
|
2
|
+
|
|
3
|
+
Exported from `errors.ts` and `health.ts`.
|
|
4
|
+
|
|
5
|
+
## Error responses — `ErrorPayloadSchema`
|
|
6
|
+
|
|
7
|
+
Every non-2xx response from the API uses this shape:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { ErrorPayloadSchema, type ErrorPayload } from '@klappay/types'
|
|
11
|
+
|
|
12
|
+
const payload: ErrorPayload = {
|
|
13
|
+
error: {
|
|
14
|
+
code: 'validation_error',
|
|
15
|
+
message: 'amount must be positive',
|
|
16
|
+
param: 'amount',
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `code` — a stable, machine-readable identifier (e.g.
|
|
22
|
+
`validation_error`, `invalid_credentials`, `invalid_trigger_state`).
|
|
23
|
+
Safe to branch on in code. There's no single enum of every possible
|
|
24
|
+
value here (it's a plain `string`, since the set is per-endpoint) —
|
|
25
|
+
the live OpenAPI/Scalar reference is the authoritative source for
|
|
26
|
+
which codes a specific endpoint can return: every response in its
|
|
27
|
+
`responses` block names the actual code(s) in parentheses at the end
|
|
28
|
+
of the description.
|
|
29
|
+
- `message` — human-readable, safe to log or show a developer. Not meant
|
|
30
|
+
for end users verbatim.
|
|
31
|
+
- `param` — which request field the error refers to, when applicable
|
|
32
|
+
(omitted for errors that aren't about a specific field).
|
|
33
|
+
|
|
34
|
+
If you want a typed exception thrown automatically instead of parsing
|
|
35
|
+
this shape yourself, an official SDK does that — available at
|
|
36
|
+
[github.com/klappay](https://github.com/klappay); see the live API
|
|
37
|
+
reference for details on the full list of error classes.
|
|
38
|
+
|
|
39
|
+
## `GET /v1/health` — `HealthSchema`
|
|
40
|
+
|
|
41
|
+
`HealthSchema`/`Health` is the response shape of the public health-check
|
|
42
|
+
endpoint. `status`/the HTTP status code both mirror `db`: `error` (and
|
|
43
|
+
`503`) when the database connectivity check fails, so a plain
|
|
44
|
+
status-code-only uptime check still catches a DB outage without needing
|
|
45
|
+
to inspect the JSON body.
|
|
46
|
+
|
|
47
|
+
The other fields are operational signals, not just "is the process
|
|
48
|
+
alive": `pendingWebhooks` (deliveries still awaiting a successful
|
|
49
|
+
attempt), `oldestPendingChargeAgeSeconds` (age of the oldest still-unpaid
|
|
50
|
+
charge, `null` if none), and `lastMoralisEventAgeSeconds` (seconds since
|
|
51
|
+
the last on-chain payment notification was received — a cheap proxy for
|
|
52
|
+
"is payment detection still working", `null` if none have ever arrived).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @klappay/types zod
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`zod` is a peer expectation, not bundled — every schema exported here
|
|
10
|
+
*is* a `zod` schema, so if you already depend on any of them directly you
|
|
11
|
+
already have it.
|
|
12
|
+
|
|
13
|
+
## What this package actually is
|
|
14
|
+
|
|
15
|
+
Two things, exported side by side for every resource:
|
|
16
|
+
|
|
17
|
+
- A **Zod schema** (`ChargeSchema`, `CreateChargeSchema`,
|
|
18
|
+
`WebhookPayloadSchema`, ...) — use it to validate data at runtime, not
|
|
19
|
+
just to satisfy the type checker.
|
|
20
|
+
- An **inferred TypeScript type** for the same shape (`Charge`,
|
|
21
|
+
`CreateChargeInput`, ...), exported alongside its schema.
|
|
22
|
+
|
|
23
|
+
Both come from the same source — the schema is the source of truth, the
|
|
24
|
+
type is `z.infer<typeof Schema>` on top of it. There is no separate
|
|
25
|
+
hand-written type anywhere in this package that could drift from its
|
|
26
|
+
schema.
|
|
27
|
+
|
|
28
|
+
This package has **no HTTP client, no fetch, no networking of any
|
|
29
|
+
kind** — it only describes shapes. If you want a client that actually
|
|
30
|
+
calls the API for you (with typed errors, retries, real-time polling
|
|
31
|
+
helpers), an official SDK is built on top of this package — available
|
|
32
|
+
at [github.com/klappay](https://github.com/klappay); see the live API
|
|
33
|
+
reference for details. Use `@klappay/types` directly when
|
|
34
|
+
you're calling the API yourself (a different language's types, a
|
|
35
|
+
codegen step, validating a
|
|
36
|
+
webhook payload in a framework the SDK doesn't fit) and just want the
|
|
37
|
+
contracts.
|
|
38
|
+
|
|
39
|
+
## Typing an API response
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import type { Charge } from '@klappay/types'
|
|
43
|
+
|
|
44
|
+
async function getCharge(id: string): Promise<Charge> {
|
|
45
|
+
const res = await fetch(`${API_BASE_URL}/v1/charges/${id}`, {
|
|
46
|
+
headers: { Authorization: `Bearer ${process.env.KLAP_API_KEY}` },
|
|
47
|
+
})
|
|
48
|
+
return res.json()
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Validating a response or webhook payload at runtime
|
|
53
|
+
|
|
54
|
+
A type only helps at compile time — it says nothing about what actually
|
|
55
|
+
came back over the wire. Parse anything you don't fully control through
|
|
56
|
+
its schema:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { WebhookPayloadSchema } from '@klappay/types'
|
|
60
|
+
|
|
61
|
+
app.post('/webhooks/klap', (req, res) => {
|
|
62
|
+
const payload = WebhookPayloadSchema.parse(req.body)
|
|
63
|
+
// payload.data is `unknown` here — this schema only validates the
|
|
64
|
+
// envelope (id/event/createdAt). See webhooks.md for how to narrow
|
|
65
|
+
// `data` per event with TypedWebhookPayload.
|
|
66
|
+
res.sendStatus(200)
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This package only validates *shape* — it never verifies the
|
|
71
|
+
`X-Klap-Signature` HMAC header on a webhook request. Do that yourself
|
|
72
|
+
(or use an official SDK's `constructEvent()`, which does both at once —
|
|
73
|
+
available at [github.com/klappay](https://github.com/klappay), see the
|
|
74
|
+
live API reference for details) before trusting the payload
|
|
75
|
+
came from Klap.
|
|
76
|
+
|
|
77
|
+
## Finding the type you need
|
|
78
|
+
|
|
79
|
+
Every exported schema/type lives in exactly one topic file below — if
|
|
80
|
+
you're not sure where something is, it's one of these:
|
|
81
|
+
|
|
82
|
+
| Doc | Covers |
|
|
83
|
+
|---|---|
|
|
84
|
+
| [`charges.md`](./charges.md) | `Charge`, `CreateChargeInput`, `ListChargesInput`, `PaginatedCharges`, `ChargeStatusEvent`, plus the read-only `VerifyCharge`/`TimelineEvent` views. Also covers the shared `PaginationQuerySchema`/`paginatedSchema()` primitives every list endpoint (charges, api keys, users, webhook deliveries) is built from |
|
|
85
|
+
| [`webhooks.md`](./webhooks.md) | Every `WebhookEventType`, category grouping, `CreateWebhookInput`, `Webhook`, `WebhookPayload`, `TypedWebhookPayload`, `WebhookDelivery` |
|
|
86
|
+
| [`sandbox.md`](./sandbox.md) | `SandboxTriggerInput`, `SandboxEventTriggerInput`, `TriggerableChargeEvent` |
|
|
87
|
+
| [`auth-and-accounts.md`](./auth-and-accounts.md) | `SignupInput`/`LoginInput`/`AuthResponse`, `ApiKey`/`CreateApiKeyInput`, `User`/`UserRole`, `Organization` |
|
|
88
|
+
| [`tokens-and-networks.md`](./tokens-and-networks.md) | `Token`, `Network`, `TOKEN_ADDRESSES`, `EVM_NETWORKS`, and every other chain/token constant |
|
|
89
|
+
| [`errors-and-health.md`](./errors-and-health.md) | `ErrorPayload` (every non-2xx response shape) and `Health` |
|
|
90
|
+
|
|
91
|
+
## Where to go next
|
|
92
|
+
|
|
93
|
+
Read this package's own `README.md` first if you haven't — it covers
|
|
94
|
+
install and the two usage patterns (typing vs. runtime validation) at a
|
|
95
|
+
glance. This `docs/` folder is for finding a *specific* schema quickly
|
|
96
|
+
once you already know roughly what you're looking for.
|
package/docs/sandbox.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Sandbox
|
|
2
|
+
|
|
3
|
+
Exported from `sandbox.ts`. These are the request bodies for
|
|
4
|
+
`POST /v1/sandbox/*` — simulating events without any real on-chain
|
|
5
|
+
activity, only usable with a `test` API key.
|
|
6
|
+
|
|
7
|
+
## Triggering a charge event — `SandboxTriggerSchema`
|
|
8
|
+
|
|
9
|
+
The body of `POST /v1/sandbox/charges/{id}/trigger`:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { SandboxTriggerSchema, type TriggerableChargeEvent } from '@klappay/types'
|
|
13
|
+
|
|
14
|
+
SandboxTriggerSchema.parse({ event: 'charge.confirmed' })
|
|
15
|
+
SandboxTriggerSchema.parse({ event: 'charge.partially_paid', amount: 20 })
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`event` is a `TriggerableChargeEvent` (exported from `webhook-events.ts`,
|
|
19
|
+
re-used here rather than re-declared) — every charge event *except*
|
|
20
|
+
`charge.created` (a charge already exists by the time you have an id to
|
|
21
|
+
trigger against), `charge.paused`/`charge.reactivated` (driven by a
|
|
22
|
+
background worker on real inactivity, not a payment state), and
|
|
23
|
+
`charge.contribution_received`/`charge.contribution_settled`
|
|
24
|
+
(exclusive to `mode: 'continuous'` charges, which this endpoint doesn't
|
|
25
|
+
support simulating):
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
export const TriggerableChargeEventSchema = ChargeWebhookEventTypeSchema.exclude([
|
|
29
|
+
'charge.created',
|
|
30
|
+
'charge.paused',
|
|
31
|
+
'charge.reactivated',
|
|
32
|
+
'charge.contribution_received',
|
|
33
|
+
'charge.contribution_settled',
|
|
34
|
+
])
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Derived with `.exclude()` from the same enum `webhook-events.ts` already
|
|
38
|
+
defines, not a second hand-maintained list — a future charge event added
|
|
39
|
+
there is automatically excluded-or-included correctly here with no
|
|
40
|
+
second place to remember to update.
|
|
41
|
+
|
|
42
|
+
`amount` applies to two events: `charge.partially_paid` (the amount to
|
|
43
|
+
simulate as received so far — must be less than the charge's full
|
|
44
|
+
amount, defaults to half of it if omitted) and `charge.overpaid` (the
|
|
45
|
+
amount received — must be greater than the charge's full amount,
|
|
46
|
+
defaults to 1.5x it if omitted). Ignored for every other event. Every
|
|
47
|
+
triggerable event has
|
|
48
|
+
a real precondition on the charge's current state (e.g.
|
|
49
|
+
`charge.underpaid` requires the charge to already be
|
|
50
|
+
`partially_paid`) — triggering one out of order is a `422
|
|
51
|
+
invalid_trigger_state` response, not a silent no-op. This schema only
|
|
52
|
+
validates the request shape; the state-machine precondition is enforced
|
|
53
|
+
server-side.
|
|
54
|
+
|
|
55
|
+
## Triggering a non-charge event — `SandboxEventTriggerSchema`
|
|
56
|
+
|
|
57
|
+
The body of `POST /v1/sandbox/events/trigger` — simulates any account,
|
|
58
|
+
security, or webhook-delivery event with synthetic data against your own
|
|
59
|
+
registered webhooks, no real charge or precondition involved (unlike the
|
|
60
|
+
charge trigger above, which acts on a specific existing charge):
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { SandboxEventTriggerSchema } from '@klappay/types'
|
|
64
|
+
|
|
65
|
+
SandboxEventTriggerSchema.parse({ event: 'payout_address.changed' })
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`event` is a `NonChargeTriggerableEvent` — every `account`/`webhooks`/
|
|
69
|
+
`security` category event (see [`webhooks.md`](./webhooks.md)'s category
|
|
70
|
+
table), also exported from `webhook-events.ts`.
|
|
71
|
+
|
|
72
|
+
## See also
|
|
73
|
+
|
|
74
|
+
- [`webhooks.md`](./webhooks.md) — the full event vocabulary these two
|
|
75
|
+
schemas draw from.
|
|
76
|
+
- An official SDK ([github.com/klappay](https://github.com/klappay))
|
|
77
|
+
offers ergonomic, named wrappers (`sandbox.confirm()`,
|
|
78
|
+
`sandbox.expire()`, ...) built on top of these two raw request shapes,
|
|
79
|
+
plus `charge.waitFor(event)` to await the result — see the live API
|
|
80
|
+
reference for details.
|