@klappay/types 1.0.1 → 1.0.2

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 CHANGED
@@ -1,5 +1,20 @@
1
1
  # @klappay/types
2
2
 
3
+ ## 1.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 00dd00a: No code changes — updates package metadata and docs links so npm shows
8
+ them correctly:
9
+
10
+ - Adds `keywords` for npm search discoverability.
11
+ - `README.md`'s documentation table now links to
12
+ [api.klappay.com/docs](https://api.klappay.com/docs) (a dedicated
13
+ public docs site) instead of relative `./docs/*.md` paths, which never
14
+ rendered as navigable links on npmjs.com.
15
+ - `docs/` is no longer bundled in the published tarball — the public
16
+ docs site above is now the canonical place to read it.
17
+
3
18
  ## 1.0.1
4
19
 
5
20
  ### Patch Changes
package/README.md CHANGED
@@ -54,16 +54,21 @@ API itself (open the API's base URL in a browser once you have access).
54
54
 
55
55
  ## Documentation
56
56
 
57
+ Full docs (with search and navigation) are published at
58
+ [api.klappay.com/docs](https://api.klappay.com/docs) — generated from
59
+ this same package's `docs/` source, so they never drift from what's
60
+ actually exported.
61
+
57
62
  | Doc | Covers |
58
63
  |---|---|
59
- | [`docs/getting-started.md`](./docs/getting-started.md) | Install, what's in this package, typing vs. runtime validation |
60
- | [`docs/charges.md`](./docs/charges.md) | `Charge`, `CreateChargeInput`, `AcceptedPayment`, `Capabilities` (`GET /v1/networks`), listing/pagination, `VerifyCharge`, `TimelineEvent` |
61
- | [`docs/webhooks.md`](./docs/webhooks.md) | Every `WebhookEventType`, categories, `Webhook`, `WebhookPayload`, `TypedWebhookPayload` |
62
- | [`docs/sandbox.md`](./docs/sandbox.md) | `SandboxTriggerInput`, `SandboxEventTriggerInput`, `TriggerableChargeEvent` |
63
- | [`docs/distributions.md`](./docs/distributions.md) | `PendingDistribution`, `PendingDistributionEvent` — the 0xSplits keeper feed |
64
- | [`docs/auth-and-accounts.md`](./docs/auth-and-accounts.md) | Signup/login, email verification, password reset, `ApiKey`, `User`/`UserRole`, `Organization` |
65
- | [`docs/tokens-and-networks.md`](./docs/tokens-and-networks.md) | `Token`, `Network`, and every chain/token constant |
66
- | [`docs/errors-and-health.md`](./docs/errors-and-health.md) | `ErrorPayload` (every non-2xx response shape), `Health` |
64
+ | [Getting started](https://api.klappay.com/docs/getting-started) | Install, what's in this package, typing vs. runtime validation |
65
+ | [Charges](https://api.klappay.com/docs/charges) | `Charge`, `CreateChargeInput`, `AcceptedPayment`, `Capabilities` (`GET /v1/networks`), listing/pagination, `VerifyCharge`, `TimelineEvent` |
66
+ | [Webhooks](https://api.klappay.com/docs/webhooks) | Every `WebhookEventType`, categories, `Webhook`, `WebhookPayload`, `TypedWebhookPayload` |
67
+ | [Sandbox](https://api.klappay.com/docs/sandbox) | `SandboxTriggerInput`, `SandboxEventTriggerInput`, `TriggerableChargeEvent` |
68
+ | [Distributions](https://api.klappay.com/docs/distributions) | `PendingDistribution`, `PendingDistributionEvent` — the 0xSplits keeper feed |
69
+ | [Auth & accounts](https://api.klappay.com/docs/auth-and-accounts) | Signup/login, email verification, password reset, `ApiKey`, `User`/`UserRole`, `Organization` |
70
+ | [Tokens & networks](https://api.klappay.com/docs/tokens-and-networks) | `Token`, `Network`, and every chain/token constant |
71
+ | [Errors & health](https://api.klappay.com/docs/errors-and-health) | `ErrorPayload` (every non-2xx response shape), `Health` |
67
72
 
68
73
  ## Usage
69
74
 
package/package.json CHANGED
@@ -1,8 +1,22 @@
1
1
  {
2
2
  "name": "@klappay/types",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "license": "MIT",
5
5
  "description": "TypeScript types and Zod schemas for the Klap Core API — the request/response contracts, published separately so integrators get them without needing access to the (closed-source) API implementation.",
6
+ "keywords": [
7
+ "klap",
8
+ "payments",
9
+ "crypto-payments",
10
+ "web3",
11
+ "blockchain",
12
+ "non-custodial",
13
+ "0xsplits",
14
+ "usdc",
15
+ "usdt",
16
+ "typescript",
17
+ "zod",
18
+ "types"
19
+ ],
6
20
  "main": "./dist/index.js",
7
21
  "types": "./dist/index.d.ts",
8
22
  "exports": {
@@ -14,7 +28,6 @@
14
28
  },
15
29
  "files": [
16
30
  "dist",
17
- "docs",
18
31
  "LICENSE",
19
32
  "README.md",
20
33
  "CHANGELOG.md"
@@ -1,188 +0,0 @@
1
- # Auth and accounts
2
-
3
- Everything here needs a **session token** (a JWT, sent as
4
- `Authorization: Bearer <token>`), not an API key — a separate credential
5
- from the one used on `/v1/charges`/`/v1/webhooks`/`/v1/sandbox`. See
6
- [`tokens-and-networks.md`](./tokens-and-networks.md) for `Token`/
7
- `Network`, which are unrelated to this auth split despite the similar
8
- name.
9
-
10
- **A session token identifies only the signed-in user — never a single
11
- organization or role.** A user can belong to any number of
12
- organizations, each with its own role, so nothing below assumes "the"
13
- organization; every organization-scoped endpoint takes its id as an
14
- explicit `{id}` path segment, and `GET /v1/organizations` is how you
15
- discover which id(s) you have.
16
-
17
- ## Signup and login — `auth.ts`
18
-
19
- `SignupSchema`/`SignupInput` and `LoginSchema`/`LoginInput` — both
20
- `{ email, password }`, bodies of `POST /v1/auth/signup` and
21
- `POST /v1/auth/login`. `email` is trimmed, lowercased, and
22
- NFC-normalized before validation (internally, via a shared
23
- `NormalizedEmailSchema` not exported on its own) — `"User@x.com"` and
24
- `"user@x.com"` are treated as the same account, matching the API's own
25
- case-insensitive uniqueness check. `signup` also creates a brand-new
26
- organization, with the new user as its `owner`; `login` doesn't select
27
- an organization at all.
28
-
29
- `AuthResponseSchema`/`AuthResponse` is what `signup`, `login`, and
30
- `POST /v1/invitations/accept` (below) all return: `{ token, user }`.
31
- `token` is a session JWT valid 7 days — this authenticates a
32
- human/dashboard session, not payment operations. `user` is pure identity
33
- — `UserSchema` (see below) minus `createdAt` **and** `role`, since role
34
- only means something once you've picked which organization. Call
35
- `GET /v1/organizations` right after to find out which organization(s)
36
- you belong to and your role in each, then create an API key
37
- (`POST /v1/organizations/{id}/api-keys`, below) before you can create
38
- charges.
39
-
40
- ## Email verification and password reset — `auth.ts`
41
-
42
- `VerifyEmailSchema`/`VerifyEmailInput` — `{ token }`, body of
43
- `POST /v1/auth/verify-email`. The token comes from the email `POST
44
- /v1/auth/signup` sends in the background (or a fresh one from
45
- `POST /v1/auth/resend-verification`, session-authenticated, no body) —
46
- single-use, expires 24h after issuance. Two things are gated on it:
47
- creating a `live` API key (`POST /v1/organizations/{id}/api-keys`,
48
- below) and changing `payoutAddress`
49
- (`PATCH /v1/organizations/{id}`, below) both require a verified email,
50
- returning `403 email_not_verified` otherwise — `test` API keys have no
51
- such requirement. `User.emailVerifiedAt` (see `UserSchema` below)
52
- reflects the current state.
53
-
54
- `ForgotPasswordSchema`/`ForgotPasswordInput` — `{ email }`, body of
55
- `POST /v1/auth/forgot-password`. Always get the same generic
56
- `MessageResponse` back regardless of whether the email matches an
57
- account — this is the one endpoint in the API that actually closes
58
- account enumeration, rather than accepting it as a tradeoff (unlike
59
- signup's `422 email_taken`).
60
-
61
- `ResetPasswordSchema`/`ResetPasswordInput` — `{ token, newPassword }`,
62
- body of `POST /v1/auth/reset-password`. The token expires 1 hour after a
63
- `forgot-password` call. A successful reset invalidates every other
64
- outstanding reset token for that account, **and every session token
65
- issued before the reset** — the next request with an old session gets
66
- `401 invalid_session`, even though the JWT itself hasn't reached its
67
- 7-day expiry.
68
-
69
- `MessageResponseSchema`/`MessageResponse` — `{ message }`, the shared
70
- response shape for all four of the endpoints above (chosen over an
71
- empty `204` since a human-readable confirmation is more useful to show
72
- directly in a UI for these specifically).
73
-
74
- ## Organizations — `organization.ts`
75
-
76
- `ListOrganizationsSchema`/`PaginatedOrganizationsSchema` are the
77
- query/response shapes for `GET /v1/organizations` — every organization
78
- the caller belongs to, each entry an `OrganizationWithRoleSchema` (see
79
- below). Same shared `{ limit, cursor }` → `{ data, nextCursor, hasMore }`
80
- cursor pagination every list endpoint uses, see
81
- [`charges.md`](./charges.md#the-shared-cursor-pagination-pattern). This
82
- is the starting point for every other endpoint on this page — there's no
83
- other way to learn which organization id(s) a session token can act on.
84
-
85
- `UpdateOrganizationSchema`/`UpdateOrganizationInput` — `{ name?,
86
- payoutAddress? }`, body of `PATCH /v1/organizations/{id}`.
87
- `payoutAddress` is an EVM address, regex-validated
88
- (`/^0x[a-fA-F0-9]{40}$/`); required before that organization can create
89
- any charge, and requires a verified email when actually changing
90
- (`403 email_not_verified` otherwise — see the email verification section
91
- above). Changing it only affects charges created **after** the change —
92
- an already-created charge's payout split is frozen at creation time and
93
- is never retroactively affected. `name` has no such gate.
94
-
95
- `OrganizationSchema`/`Organization` is the read shape returned by
96
- `GET`/`PATCH /v1/organizations/{id}` — notably `currentFeePercent`, the
97
- organization's platform fee based on trailing monthly volume, which is
98
- likewise frozen onto each charge at creation (so it can change between
99
- charges without affecting ones already made).
100
- `OrganizationWithRoleSchema`/`OrganizationWithRole` extends it with
101
- `role` — only present on the `GET /v1/organizations` list response,
102
- where "your role in *this* one" is meaningful; the single-organization
103
- `GET`/`PATCH` endpoints don't repeat it.
104
-
105
- ## Members and roles — `users.ts`
106
-
107
- `UserRoleSchema`/`UserRole` — `'owner' | 'admin' | 'member'`. An
108
- organization must always keep at least one `owner`. An `admin` can
109
- manage `member`s but not other `admin`s; you can only manage a user with
110
- a strictly lower role than your own, unless you're an `owner` — this
111
- applies identically to changing a role and to inviting one (see
112
- "Invitations" below).
113
-
114
- `UpdateUserRoleSchema`/`UpdateUserRoleInput` — `{ role }`, body of
115
- `PATCH /v1/organizations/{id}/users/{userId}`, for changing an existing
116
- member's role *within that organization*. `UserSchema`/`User` is the
117
- read shape returned there and by `GET /v1/organizations/{id}/users` —
118
- `role` is that member's role in the organization the request was scoped
119
- to (a `User` has no single global role, since the same person can be a
120
- `member` of one organization and an `owner` of another).
121
- `emailVerifiedAt` (nullable timestamp) reflects whether the address was
122
- confirmed via `POST /v1/auth/verify-email` — it gates two things
123
- elsewhere in the API (creating a `live` API key, changing
124
- `payoutAddress` — see above) but doesn't restrict a `member` based on
125
- role by itself; it's there for your own integration's policy to read
126
- too.
127
-
128
- `ListUsersSchema`/`PaginatedUsersSchema` are the query/response shapes
129
- for `GET /v1/organizations/{id}/users` — same shared cursor pagination
130
- as `GET /v1/organizations` above.
131
-
132
- ## Invitations — `invitations.ts`
133
-
134
- `InviteUserSchema`/`InviteUserInput` — `{ email, role }` (`role` defaults
135
- to `member`), body of `POST /v1/organizations/{id}/invitations`. Subject
136
- to the identical role-hierarchy rule as `UpdateUserRoleSchema` above —
137
- an `admin` inviter can't invite an `admin` or `owner`. Sends a
138
- single-use, plain-text email code (via Resend, no clickable link — same
139
- pattern as email verification/password reset), valid 7 days.
140
- `InvitationSchema`/`Invitation` is the response shape — `{ id,
141
- organizationId, email, role, invitedByUserId, expiresAt, createdAt }`,
142
- no token (the raw code only ever reaches the invitee, via email). Keep
143
- the returned `id` if you need to
144
- `DELETE /v1/organizations/{id}/invitations/{invitationId}` later.
145
-
146
- `AcceptInvitationSchema`/`AcceptInvitationInput` — `{ token,
147
- password? }`, body of `POST /v1/invitations/accept`. **No session
148
- token required** — the code itself is the proof, same model as
149
- `VerifyEmailSchema` above. If the invited email already has a Klap
150
- account, `password` is ignored and the membership is just added to it
151
- (any other organizations that account already belongs to are
152
- untouched); if it doesn't, `password` is required (same `8-128`-char
153
- rule as `SignupSchema`) and a new account is created along with the
154
- membership, in one step. Returns an `AuthResponse` either way.
155
-
156
- ## API keys — `api-keys.ts`
157
-
158
- `CreateApiKeySchema`/`CreateApiKeyInput` — `{ name, environment }`, the
159
- body of `POST /v1/organizations/{id}/api-keys`. `environment`
160
- (`live`/`test`, see [`charges.md`](./charges.md)'s `EnvironmentSchema`)
161
- decides everything about the resulting key: `live` keys move real funds
162
- on Base mainnet; `test` keys settle the same charge lifecycle on Base
163
- Sepolia (a real testnet — real on-chain activity, never real money) and
164
- additionally unlock `POST /v1/sandbox/*` (see [`sandbox.md`](./sandbox.md))
165
- for simulating events with zero on-chain activity at all. Creating a
166
- `live` key requires a verified email (`403 email_not_verified`
167
- otherwise) — `test` keys have no such requirement.
168
-
169
- `ApiKeySchema`/`ApiKey` is the response shape. `key` (the full secret,
170
- `klap_live_.../klap_test_...`) is present **only** in the response to
171
- creation — every later read/list returns `hint` instead (a
172
- truncated, always-safe-to-display form like `klap_live_...ab12`). Store
173
- `key` immediately; it cannot be retrieved again.
174
-
175
- `ListApiKeysSchema`/`PaginatedApiKeysSchema` are the query/response
176
- shapes for `GET /v1/organizations/{id}/api-keys` — same shared
177
- `{ limit, cursor }` → `{ data, nextCursor, hasMore }` cursor pagination
178
- every list endpoint uses, see
179
- [`charges.md`](./charges.md#the-shared-cursor-pagination-pattern).
180
-
181
- ## See also
182
-
183
- - [`charges.md`](./charges.md) — `EnvironmentSchema` (`live`/`test`),
184
- referenced by both API keys and charges.
185
- - [`webhooks.md`](./webhooks.md) — the `account`/`security` category
186
- events these resources emit (`payout_address.changed`,
187
- `api_key.created`, `member.role_changed`, `member.invited`,
188
- `auth.login`, ...).
package/docs/charges.md DELETED
@@ -1,303 +0,0 @@
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.
@@ -1,93 +0,0 @@
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.
@@ -1,52 +0,0 @@
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).
@@ -1,96 +0,0 @@
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 DELETED
@@ -1,80 +0,0 @@
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.
@@ -1,108 +0,0 @@
1
- # Tokens and networks
2
-
3
- Exported from `tokens.ts` and `networks.ts`.
4
-
5
- ## `Token`
6
-
7
- `TokenSchema`/`Token` — currently `'USDC' | 'USDT'`. This is today's
8
- supported list, not a permanent ceiling — more tokens are expected to be
9
- added over time as new networks and assets come online. Support depends
10
- on both `network` and `environment`, and not every combination is
11
- symmetric: `live` has both `USDC` and `USDT` on every operational
12
- network *except* `bnb` (see the `USDC`/`bnb` note below), and `test`
13
- coverage varies per network for different reasons — `base`, `optimism`,
14
- and `ethereum` each have a `test` environment with `USDC` only (none has
15
- an official Sepolia USDT, since Tether doesn't issue one), while
16
- `arbitrum`, `polygon`, and `avalanche` have no `test` environment at all
17
- yet, for any token (0xSplits hasn't deployed its split factory on
18
- Arbitrum Sepolia, and has no Polygon or Avalanche Fuji testnet support at
19
- all — a network-level gap, not a token-level one), and `bnb` has none
20
- either (no BNB testnet support in 0xSplits). An unconfigured combination
21
- is rejected with `422 token_not_supported`, not silently accepted. See
22
- `TOKEN_ADDRESSES` below for the exact current matrix rather than
23
- assuming full coverage.
24
-
25
- `TOKEN_DECIMALS` (`6`) — both tokens Klap supports use 6 decimals on
26
- every network they're deployed on. A fact about these specific tokens,
27
- not a per-network setting, so it's one shared constant rather than
28
- something you'd look up per-`Network`.
29
-
30
- `TOKEN_ADDRESSES: Record<Token, Partial<Record<Network, Partial<Record<Environment, `0x${string}`>>>>>`
31
- — the real on-chain contract address for each token/network/environment
32
- combination that's actually deployed. Three levels deep because `test`
33
- and `live` are genuinely different chains for at least one network today
34
- (`base` → Base mainnet for `live`, Base Sepolia for `test` — see
35
- [`charges.md`](./charges.md) for `Environment`), so the same token can
36
- have a different contract address per environment, not just per network.
37
- `Partial` at every level matters: index it as
38
- `TOKEN_ADDRESSES[token][network]?.[environment]` and check for
39
- `undefined`, since a combination that doesn't exist (e.g. no official
40
- Base Sepolia USDT, or any `arbitrum` `test` entry at all) has no entry
41
- rather than a placeholder value. One naming quirk on `arbitrum`: the
42
- `USDT` contract there is real and Tether-backed, but reports its
43
- on-chain name/`symbol()` as `"USD₮0"` (Tether's "USDT0" cross-chain
44
- standard, migrated in place in Jan 2025) — this codebase always verifies
45
- by address, never by `symbol()`, so it doesn't matter functionally, but
46
- don't be surprised seeing "USD₮0" instead of "USDT" on a block explorer.
47
-
48
- **`USDC` on `bnb` is Binance-Peg USDC, not a Circle deployment.** Circle
49
- issues no native USDC on BNB Chain at all — it's absent from both
50
- Circle's own address list and its CCTP-supported chain list. The address
51
- in `TOKEN_ADDRESSES` is a Binance-custodied, 1:1-pegged BEP-20 token
52
- instead (Binance locks real USDC/equivalent collateral in its own wallet
53
- and mints this against it) — real liquidity and adoption (it's what
54
- "USDC" means on BNB Chain in practice), but a centralized-custody trust
55
- model, not the direct-issuer verification every other `USDC`/`USDT`
56
- entry in this table has. See `docs/payments.md`'s note on it before
57
- building anything that treats every `TOKEN_ADDRESSES` entry as
58
- equally trusted. `USDT` on `bnb` is Tether's own official BEP-20
59
- issuance — same trust model as everywhere else.
60
-
61
- ## `Network`
62
-
63
- `NetworkSchema`/`Network` — `'base' | 'optimism' | 'polygon' |
64
- 'ethereum' | 'arbitrum' | 'avalanche' | 'bnb'`. This is the full type —
65
- every one of these values is a valid `Network` for reading/filtering
66
- (e.g. `ListChargesInput.network`). `solana` used to be part of this type
67
- but was removed entirely (not just left non-operational) once there was
68
- no near-term plan to wire it — see `docs/security.md`'s finding #13.
69
-
70
- `OPERATIONAL_NETWORKS` (`readonly ['base', 'arbitrum', 'optimism',
71
- 'polygon', 'ethereum', 'avalanche', 'bnb']`) and its matching
72
- `OperationalNetwork` type — the narrower list of networks actually
73
- wired end-to-end today, which right now is exactly every `Network`
74
- value. `CreateChargeSchema.network` enforces this at creation: `POST
75
- /v1/charges` with any `network` outside this list is rejected with
76
- `400 validation_error` naming the network and what's currently
77
- supported, not silently accepted and left to strand funds. Not a
78
- permanent ceiling and not guaranteed to stay in lockstep with
79
- `Network` — the day a genuinely new chain is added to the type before
80
- its wiring lands, this list will again be the narrower one. Being in
81
- `OPERATIONAL_NETWORKS` means `live` works; it says nothing about `test`
82
- on its own — see `Token` above for why
83
- `arbitrum`/`polygon`/`avalanche`/`bnb` specifically have no `test`
84
- environment yet (`optimism`/`ethereum` do).
85
-
86
- `EVM_NETWORKS` (`readonly ['base', 'optimism', 'polygon', 'ethereum',
87
- 'arbitrum', 'avalanche', 'bnb']`) and its matching `EvmNetwork` type —
88
- every current `Network` value (all EVM-based, now that `solana` is
89
- gone). Use this to guard any logic that assumes an EVM-style
90
- address/RPC — a future non-EVM network would reintroduce a real gap
91
- here.
92
-
93
- `NETWORK_LABELS: Record<Network, string>` — display names (`'Base'`,
94
- `'Optimism'`, ...) for UI use.
95
-
96
- `NETWORK_EXPLORERS: Record<Network, string>` — each network's block
97
- explorer base URL (e.g. `https://basescan.org`), used to build a direct
98
- transaction link (see `explorerTxUrl` on `VerifyCharge`, in
99
- [`charges.md`](./charges.md)).
100
-
101
- ## See also
102
-
103
- - [`charges.md`](./charges.md) — where `Token`/`Network` are actually
104
- used (`CreateChargeInput.acceptedPayments`, `Charge.acceptedPayments`/
105
- `paidWith`, `VerifyCharge`), and `GET /v1/networks`
106
- (`CapabilitiesSchema`) — the live version of this same matrix, scoped
107
- to your API key's `environment`, rather than the full type-level
108
- listing on this page.
package/docs/webhooks.md DELETED
@@ -1,165 +0,0 @@
1
- # Webhooks
2
-
3
- Everything here is exported from `@klappay/types`'s webhook-related
4
- modules (`webhook-events.ts`, `webhooks.ts`, `webhook-event-data.ts`) —
5
- import from the package root either way, this split is an internal
6
- implementation detail.
7
-
8
- ## Event types, and the four categories
9
-
10
- Every event Klap can send is one `WebhookEventTypeSchema` value, built
11
- from four sub-enums (each independently exported, useful if you only
12
- ever care about one group):
13
-
14
- | Sub-schema | Category | Events |
15
- |---|---|---|
16
- | `ChargeWebhookEventTypeSchema` | `payments` | `charge.created`, `charge.partially_paid`, `charge.confirmed`, `charge.expired`, `charge.underpaid`, `charge.settled`, `charge.settlement_failed`, `charge.overpaid`, `charge.paused`, `charge.reactivated`, `charge.contribution_received`, `charge.contribution_settled` |
17
- | `AccountWebhookEventTypeSchema` | `account` | `payout_address.changed`, `api_key.created`, `api_key.revoked`, `webhook.created`, `webhook.deleted`, `webhook.secret_rotated`, `fee_tier.updated`, `member.removed`, `member.role_changed`, `member.invited` |
18
- | `WebhookDeliveryEventTypeSchema` | `webhooks` | `webhook.delivery_failed`, `webhook.delivery_recovered`, `webhook.endpoint_unhealthy` |
19
- | `SecurityWebhookEventTypeSchema` | `security` | `auth.login`, `auth.login_failed`, `auth.suspicious_activity`, `auth.email_verified`, `auth.password_reset_requested`, `auth.password_reset_completed` |
20
-
21
- `EVENT_CATEGORY_MAP` (`Record<WebhookEventType, WebhookCategory>`) is the
22
- single source of truth for which category an event belongs to —
23
- generated from the four enums above, never hand-duplicated. Its inverse,
24
- `WEBHOOK_EVENT_CATEGORIES` (`Record<WebhookCategory, readonly
25
- WebhookEventType[]>`), gives you every event in a category, e.g. for
26
- building a subscription UI.
27
-
28
- Two events worth calling out specifically:
29
-
30
- - **`charge.confirmed` vs. `charge.settled`** — `confirmed` means the
31
- payment was *detected on-chain*; `settled` means the merchant's wallet
32
- *actually received* the funds, a separate, later step (see
33
- `settlementStatus` in [`charges.md`](./charges.md)). Subscribe to
34
- `confirmed` if you only need "will I get paid", or `settled` if you
35
- need "has the money actually arrived".
36
- - **`charge.overpaid`** fires *alongside* `charge.confirmed`/
37
- `charge.partially_paid` whenever the cumulative amount received ends
38
- up above `amount` — it's an additional signal, not a replacement
39
- status.
40
- - **`charge.paused`/`charge.reactivated`/`charge.contribution_received`/
41
- `charge.contribution_settled`** are the only events in this category
42
- that don't carry the full `Charge` object as `data`. `paused`/
43
- `reactivated` only ever fire for a charge with no `expiresAt` that's
44
- gone inactive for longer than its inactivity window, carrying
45
- `{ chargeId, lastActivityAt, pausedAt }` / `{ chargeId,
46
- reactivatedAt }`. `contribution_received`/`contribution_settled` are
47
- exclusive to `mode: 'continuous'` charges (see `mode` in
48
- [`charges.md`](./charges.md)) — a continuous charge never fires
49
- `charge.confirmed`/`charge.settled` at all, since `status` never
50
- leaves `pending`; instead every individual transfer fires
51
- `contribution_received` on detection and `contribution_settled` once
52
- its payout completes, carrying `{ chargeId, token, network, amount,
53
- txHash, payerAddress }` / `{ chargeId, token, network, amount,
54
- txHash, distributorAddress }` — one pair-scoped event per
55
- contribution instead of one event for the whole charge.
56
-
57
- `member.invited` does not exist — there's no invite endpoint in the API
58
- today.
59
-
60
- ## Subscribing — `CreateWebhookSchema`
61
-
62
- `POST /v1/webhooks`'s body. At least one of `events` or `eventCategories`
63
- is required (enforced by a `.refine()`, not just documentation):
64
-
65
- ```ts
66
- import { CreateWebhookSchema } from '@klappay/types'
67
-
68
- CreateWebhookSchema.parse({
69
- url: 'https://example.com/webhooks/klap',
70
- eventCategories: ['payments'],
71
- })
72
- ```
73
-
74
- - `events` — individual event types, or `"*"` (`WEBHOOK_EVENTS_WILDCARD`)
75
- for every event.
76
- - `eventCategories` — subscribe to a whole category at once (see the
77
- table above) — new events added to that category later arrive
78
- automatically, no subscription update needed.
79
- - `excludeEvents` — opt back out of specific events even under a `"*"`
80
- or category subscription.
81
- - `url` must be HTTPS and resolve to a public address — private/internal
82
- IPs are rejected server-side.
83
-
84
- `WebhookSchema` / `Webhook` is the response to creating one — critically,
85
- `secret` (the HMAC signing secret) is only ever present in **this**
86
- response. `WebhookListItemSchema` / `WebhookListItem` is what every
87
- subsequent list/read returns instead: the same shape minus `secret`,
88
- plus `hint` (a truncated, safe-to-display form, e.g. `whsec_...ab12`).
89
-
90
- ## The payload envelope
91
-
92
- `WebhookPayloadSchema` / `WebhookPayload` is the wire shape of every
93
- delivery: `{ id, event, createdAt, data }`. `data` is typed `unknown`
94
- here on purpose — its real shape depends on `event`, which a single flat
95
- schema can't express. `id` is also sent as the `X-Klap-Delivery` header,
96
- distinct from the delivery-id-per-registered-webhook used internally.
97
-
98
- For the *typed*, narrowed version, use `TypedWebhookPayload` — a
99
- discriminated union over `event` (same pattern as Stripe's
100
- `Event.data.object`):
101
-
102
- ```ts
103
- import type { TypedWebhookPayload } from '@klappay/types'
104
-
105
- function handle(payload: TypedWebhookPayload) {
106
- if (payload.event === 'charge.confirmed') {
107
- payload.data.amountReceived // typed as Charge, no cast needed
108
- } else if (payload.event === 'payout_address.changed') {
109
- payload.data.to // typed as PayoutAddressChangedData
110
- }
111
- }
112
- ```
113
-
114
- `WebhookEventDataMap` is the underlying `{ event: dataShape }` mapping
115
- `TypedWebhookPayload` is built from — every `charge.*` event carries a
116
- full `Charge`; every other event carries a small, event-specific object
117
- (all plain TypeScript types, not Zod schemas, since they're never
118
- validated standalone, only as part of `TypedWebhookPayload`):
119
-
120
- | Event(s) | `data` shape |
121
- |---|---|
122
- | `payout_address.changed` | `PayoutAddressChangedData` — `{ organizationId, from, to }` (`from` nullable) |
123
- | `api_key.created` / `api_key.revoked` | `ApiKeyEventData` — `{ apiKeyId, name, environment, hint }` |
124
- | `webhook.created` / `webhook.deleted` / `webhook.secret_rotated` | `WebhookConfigEventData` — `{ webhookId, url }` |
125
- | `webhook.delivery_failed` / `webhook.delivery_recovered` / `webhook.endpoint_unhealthy` | `WebhookHealthEventData` — `{ webhookId, url, failureRatio? }` (`failureRatio` only on `endpoint_unhealthy`) |
126
- | `fee_tier.updated` | `FeeTierUpdatedData` — `{ organizationId, previousFeePercent, newFeePercent }` |
127
- | `member.removed` | `MemberEventData` — `{ userId, email, role }` |
128
- | `member.role_changed` | `MemberRoleChangedData` — `MemberEventData & { previousRole }` |
129
- | `member.invited` | `MemberInvitedData` — `{ organizationId, email, role, invitedByUserId }` |
130
- | `auth.login` | `AuthLoginData` — `{ userId, ipAddress }` |
131
- | `auth.login_failed` | `AuthLoginFailedData` — `{ email, ipAddress }` |
132
- | `auth.suspicious_activity` | `AuthSuspiciousActivityData` — `AuthLoginData & { previousIpAddress }` |
133
- | `auth.email_verified` | `AuthEmailVerifiedData` — `{ userId, email }` |
134
- | `auth.password_reset_requested` | `AuthPasswordResetRequestedData` — `{ userId, email }` |
135
- | `auth.password_reset_completed` | `AuthPasswordResetCompletedData` — `{ userId, email }` |
136
-
137
- ## Delivery health
138
-
139
- `WebhookDeliveryStatusSchema` / `WebhookDeliveryStatus` —
140
- `'pending' | 'delivered' | 'failed'` (`failed` means all 5 retry
141
- attempts over ~24h were exhausted; retry manually via
142
- `POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry`).
143
- `WebhookDeliverySchema` / `WebhookDelivery` is one entry from `GET
144
- /v1/webhooks/{id}/deliveries` — `responseCode: null` means every attempt
145
- failed to connect at all, not just a non-2xx response.
146
- `ListWebhookDeliveriesSchema`/`PaginatedWebhookDeliveriesSchema` are the
147
- query/response shapes for that endpoint — same shared cursor pagination
148
- as `GET /v1/charges`/`GET /v1/organizations/{id}/api-keys`/
149
- `GET /v1/organizations/{id}/users`, see
150
- [`charges.md`](./charges.md#the-shared-cursor-pagination-pattern). `GET
151
- /v1/webhooks` itself (listing the webhooks, not their deliveries) stays
152
- unpaginated — it's hard-capped at 20 active webhooks per organization.
153
-
154
- The `webhook.*` meta-events (`WebhookDeliveryEventTypeSchema`) let you
155
- monitor this without polling: `webhook.endpoint_unhealthy` fires once
156
- when a webhook's trailing-24h failure rate crosses 20%,
157
- `webhook.delivery_recovered` fires once when a delivery next succeeds
158
- afterward.
159
-
160
- ## See also
161
-
162
- - [`charges.md`](./charges.md) — the `Charge` shape carried by every
163
- `charge.*` event's `data`.
164
- - [`sandbox.md`](./sandbox.md) — `TriggerableChargeEvent`, the subset of
165
- charge events you can simulate without a real on-chain transfer.