@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.
@@ -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.
@@ -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.