@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 +15 -0
- package/README.md +13 -8
- package/package.json +15 -2
- package/docs/auth-and-accounts.md +0 -188
- package/docs/charges.md +0 -303
- package/docs/distributions.md +0 -93
- package/docs/errors-and-health.md +0 -52
- package/docs/getting-started.md +0 -96
- package/docs/sandbox.md +0 -80
- package/docs/tokens-and-networks.md +0 -108
- package/docs/webhooks.md +0 -165
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
|
-
| [
|
|
60
|
-
| [
|
|
61
|
-
| [
|
|
62
|
-
| [
|
|
63
|
-
| [
|
|
64
|
-
| [
|
|
65
|
-
| [
|
|
66
|
-
| [
|
|
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.
|
|
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.
|
package/docs/distributions.md
DELETED
|
@@ -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).
|
package/docs/getting-started.md
DELETED
|
@@ -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.
|