Distributions
Exported from distributions.ts. These back GET /v1/distributions/pending and GET /v1/distributions/pending/events — the keeper feed for discovering splits with a settlement payout currently claimable via 0xSplits' permissionless distribute(). Not a typical merchant integration surface — this is for a bot/script calling distribute() directly to earn the (small) distributorFeePercent incentive.
PendingDistributionSchema
One entry in the data array GET /v1/distributions/pending returns:
ts
import { PendingDistributionSchema, type PendingDistribution } from '@klappay/types'
const distribution: PendingDistribution = PendingDistributionSchema.parse({
splitAddress: '0x727cddb8a015f76c4e8a5c566c7e911761e268fe',
network: 'base',
token: 'USDC',
recipients: [
{ address: '0x6EadA5173e0ad685455cEFbe4B3df023c0A598A8', percentAllocation: 99.0991 },
{ address: '0x44Ef725cA64CDAf48fEdb2AED2b156ad32537d41', percentAllocation: 0.9009 },
],
distributorFeePercent: 0.1,
estimatedRewardAmount: 0.001,
availableSince: '2026-07-28T23:45:52.382Z',
graceEndsAt: '2026-07-28T23:50:52.382Z',
})recipients is always the exact array to pass to distribute() — 0xSplits only stores a hash of the recipient config on-chain, so a caller must supply the identical array to prove it matches; there's no partial/reconstructed version. estimatedRewardAmount is an estimate from the amount Klappay detected on-chain, not a live balance read — always read the split's real balance yourself before submitting a transaction, the same way Klappay's own settlement worker does. graceEndsAt is when Klappay's own worker may claim it; calling distribute() after that point is still possible but increasingly likely to lose the race.
Each entry of recipients is PendingDistributionRecipientSchema / PendingDistributionRecipient — { address, percentAllocation }, exported separately so a caller building the distribute() argument list can type one recipient without destructuring the whole distribution. percentAllocation is a percentage, not a fraction (99.0991 means 99.0991%), and the array is reproduced verbatim rather than normalized — it has to hash to exactly what the split contract stores.
SplitDistributionStatusSchema / SplitDistributionStatus is a related but distinct enum: 'pending' | 'processing' | 'completed' | 'failed', the status of one payout attempt to the merchant for a single (token, network) pair. A charge settling across more than one pair has one per pair. pending is queued and unclaimed; processing means a distributor — Klappay's own worker, or anyone who raced to call distribute() first — has claimed it and is submitting the transaction; completed means the merchant's wallet has the funds; and failed means every automatic retry was exhausted. A distribution can also stay pending past its usual retry window when the token issuer restricts an address the payout needs (the split itself as sender, or a recipient that can't receive): Klappay holds it, rechecks hourly and pays out once the restriction is lifted, without sending a webhook. See token restrictions. It describes the payout attempt, whereas the schemas above describe the work still waiting to be claimed.
PaginatedPendingDistributionsSchema — the actual GET /v1/distributions/pending response
ts
import { PaginatedPendingDistributionsSchema, type PaginatedPendingDistributions } from '@klappay/types'
const page: PaginatedPendingDistributions = PaginatedPendingDistributionsSchema.parse(
await res.json(),
)
// page.data: PendingDistribution[], page.hasMore: boolean, page.nextCursor: string | nullCursor-paginated, same shape and semantics as every other list endpoint (e.g. GET /v1/charges) — pass ?limit=/?cursor= (PaginationQuerySchema) to page through it. There's no fixed row cap anymore: during an anomalous backlog (e.g. a stalled RPC inflating the number of still-claimable rows), keep following nextCursor instead of only ever seeing however many rows fit in one response.
PendingDistributionEventSchema
The shape of each Server-Sent Event's data: line from GET /v1/distributions/pending/events — a discriminated union on type:
ts
import { PendingDistributionEventSchema, type PendingDistributionEvent } from '@klappay/types'
const event: PendingDistributionEvent = PendingDistributionEventSchema.parse(
JSON.parse(sseEventData),
)
if (event.type === 'distribution.available') {
tryClaim(event.distribution) // PendingDistribution, fully typed
} else {
cancelPendingAttempt(event.splitAddress) // already claimed by someone else, or by Klappay
}distribution.claimed fires the moment Klappay's own worker claims a row too — before it has actually finished calling distribute() — not only once settlement completes, so a keeper stops racing an attempt already in flight. If that attempt then fails and is scheduled for retry, distribution.available fires again with a refreshed graceEndsAt.
By default the stream sends no initial snapshot — open it before calling GET /v1/distributions/pending, then treat both the snapshot response and every event received (before or after that call resolves) as an idempotent add/remove against one local Map<splitAddress, PendingDistribution>. Opening the stream after the snapshot call instead leaves a real, if small, gap where a delta between the two requests is never delivered.
ListenPendingDistributionsQuerySchema — a self-contained alternative
ts
import { ListenPendingDistributionsQuerySchema } from '@klappay/types'
// GET /v1/distributions/pending/events?limit=50
ListenPendingDistributionsQuerySchema.parse({ limit: '50' }) // { limit: 50 }Pass ?limit= (up to PAGINATION_LIMIT_MAX, default 0) and the connection sends up to that many currently-claimable distributions as synthetic distribution.available events immediately after connecting, before live deltas continue — one connection instead of the two-request dance above, with no ordering footgun (the subscription opens before the snapshot query runs, so nothing published in between is lost). It isn't a full page, though: if more than limit are claimable at connect time, the excess simply isn't sent — reach for the paginated GET /v1/distributions/pending directly if you need a complete listing during an anomalous backlog. Omitting ?limit= (or passing 0) keeps the original two-step behavior above unchanged.
Authentication and environment scoping
Both endpoints require the same API key used everywhere else (Authorization: Bearer klap_live_.../klap_test_...). A test key only ever sees test distributions, live only live — there is no environment field on either schema above, since the connection is already scoped by whichever key opened it.
