Send money

Pay a typed recipient, manage Commitments, and read the payment ledger.

This guide covers paying a typed recipient, managing a Commitment, and reading payments.

Before you start

  • Auth and onboarding are done (Build the auth flow) and useCapxulIdentity() is authenticated with account.at === "claimed".
  • The account holds test funds (see test keys and the faucet).
  • Skim Money & accounts for the model: AssetAmount, Account, Payment, Commitment.
  • Check Capability status. Each payments claim uses the repository's fixed evidence state and names whether proof stops at a release-trunk contract, persistence, integration, or live execution boundary.

Payment amounts are AssetAmount values. They contain an exact asset and a decimal value. They do not contain raw token units or fiat currency identity. The asset registry supplies precision. It supports six-decimal USDC and USDT as separate assets, and 18-decimal WETH.

Pay a typed recipient

useCapxulPay sends money to a typed recipient reference — a handle, an email, an organization handle, or a saved payee id:

"use client";

import { useCapxulPay } from "@capxul/sdk-react";

export function PayButton() {
  const pay = useCapxulPay();

  const handlePay = async () => {
    const payment = await pay.mutateAsync({
      to: { kind: "email", email: "dana@example.com" },
      amount: {
        asset: "eip155:84532/erc20:0x06ba8b863ca77490b0e7008427562bdbfcff730e",
        value: "25.00",
      },
    });
    // Check payment.status: an instant send to a validated recipient
    // settles; an unvalidated recipient escrows as "pending_claim".
  };

  return (
    <button type="button" onClick={() => void handlePay()} disabled={pay.isPending}>
      {pay.isPending ? "Sending…" : "Pay $25"}
    </button>
  );
}

The to reference is one of:

kindFieldsPays
"handle"handlea Capxul user by handle
"email"emailanyone by email — including people not on Capxul yet
"organization"handlean org by its handle
"payee"ida saved payee

Two constraints to design around (both by design, both graded on Capability status):

  • Bare addresses are rejected. pay never accepts a raw EVM address. That path fails with INVALID_RECIPIENT.
  • pay is personal-actor only today. Passing an organization actor fails with NOT_IMPLEMENTED — org treasury spending goes through the org surface, not payments.pay.

Optionally attach a paymentType ("invoice", "payroll", "reimbursement") or a payment document. A payment pays at once: held timing (a scheduled or stream clause) returns INVALID_INPUT on timing.

Unvalidated recipients become Commitments

One rule explains most "why is this payment not settled?" moments: an instant, irreversible send is only permitted to a validated recipient. Paying anyone not yet validated — a fresh email, an unclaimed handle — does not fail. It escrows the money in a recoverable Commitment instead:

  • The Payment comes back with status: "pending_claim".
  • The recipient claims it once they are on Capxul; availableToClaim on the payment tracks what they can take.

The SDK enforces this; it is not a bug to work around. The full model is in Money & accounts.

List payments and read one

useCapxulPayments lists the payment ledger (most recent first); useCapxulPayment reads one by id with its live release state:

"use client";

import { useCapxulPayment, useCapxulPayments } from "@capxul/sdk-react";

function PaymentsList() {
  const payments = useCapxulPayments();

  if (payments.isLoading) return <p>Loading payments…</p>;
  if (payments.isError) return <p>Could not load payments ({payments.error.code}).</p>;

  return (
    <ul>
      {payments.data?.map((payment) => (
        <li key={payment.id}>
          {payment.recipient.label} · {payment.amount.value} · {payment.status}
        </li>
      ))}
    </ul>
  );
}

function PaymentDetail({ paymentId }: { paymentId: string }) {
  const payment = useCapxulPayment(paymentId);
  if (payment.data == null) return null;
  return (
    <p>
      Released {payment.data.released.value} / {payment.data.amount.value} — claimable:{" "}
      {payment.data.availableToClaim.value}
    </p>
  );
}

A Payment is leak-safe by construction: recipient kind + label, amount, status (pending, settling, pending_claim, scheduled, streaming, settled, cancelled, failed), timing, released / availableToClaim, and document references. There is no transaction hash and no address on it.

Payment mutations invalidate the payments list and detail automatically. See TanStack Query integration.

Held payments

A payment pays at once. Held payments (scheduled releases, vesting and claims) retired with the move to the V3 payment router, so the Core SDK has no createCommitment, claim, cancel or redirect.

What you end up with

  • A pay flow that sends to handles, emails, orgs, and payees.
  • A payments screen listing the ledger and showing each payment's state.

On this page