Apps

Refunds

When your app collects payment but fails to deliver, the payer should get their money back. PayWeave handles this for you by default — any throw from your payweave.route() handler automatically triggers a refund.

Auto-refund on throw (default)

payweave.route() catches every exception from your handler and:

1. Calls payweave.refund() with reason = err.message (the thrown Error's message).

2. Returns 502 { error: err.message, message: err.message } to the caller.

Write short, machine-readable Error messages so refund records and response codes stay useful for analytics:

TypeScript
payweave.route(app, 'post', '/api/weather', {
  price: '0.001',
  inputSchema: WeatherInput,
  handler: async (_c, body) => {
    const res = await fetch(`https://api.weather/?q=${body.city}`);
    if (!res.ok) throw new Error('weather_upstream_error'); // ← becomes refund reason
    return await res.json();
  },
});

Opting out

Pass refundOnError: false to disable auto-refund. Throws propagate normally to the framework's error handler — the charge stands.

TypeScript
payweave.route(app, 'post', '/api/debug', {
  price: '0.001',
  refundOnError: false,
  handler: async () => {
    throw new Error('work in progress'); // caller sees 500, no refund
  },
});

Manual refund (fine-grained control)

Disable auto-refund and call payweave.refund() yourself when you need different HTTP status codes or want to refund only specific error kinds.

TypeScript
payweave.route(app, 'post', '/api/weather', {
  price: '0.001',
  refundOnError: false,
  inputSchema: WeatherInput,
  handler: async (c, body) => {
    try {
      return await fetchWeather(body.city);
    } catch (err: any) {
      if (err.status === 429) {
        // Upstream rate-limited us — refund + tell caller to retry
        await payweave.refund(c.env, {
          transactionHash: c.get('payweaveTransactionHash')!,
          reason: 'upstream_rate_limited',
        });
        return c.json({ error: 'rate_limited', retry_after: 60 }, 429);
      }
      if (err.status === 400) {
        // User's fault (invalid symbol that passed our Zod) — don't refund
        return c.json({ error: 'bad_request', message: err.message }, 400);
      }
      throw err; // any other failure → framework's 500
    }
  },
});

Refund API

Auto-refund and manual refund both end up calling the same platform endpoint. If you need to refund from outside a handler (e.g. a background job), call it directly:

POST/api/mpp/app/:appId/refund

Authenticated with the same appSecret Bearer token used for charges. Accepts either a paymentId or a transactionHash.

JSON
{
  "transactionHash": "0xabc...def",
  "reason": "upstream_rate_limited"
}

Response

JSON
{
  "success": true,
  "refundId": "f1b2c3d4-...",
  "paymentId": "a3cdf4ea-...",
  "refundAmountUsd": "0.01000000",
  "payerAddress": "did:pkh:eip155:42431:0x...",
  "status": "pending"
}

Refund status lifecycle

A refund starts pending. The workspace owner reviews and signs it from the dashboard — then it moves through processing to settled (or failed).

What auto-refund does NOT catch

ScenarioRefunded?Why
Handler returns successfullyNoNothing threw. Payment stands.
Body validation failure (400)N/ACharge never happened — invalid bodies fail before the handler runs.
402 challenge probeN/ANo payment taken on the probe.
Handler throws (any reason)Yes, by defaultAuto-refund fires with reason = err.message.
Handler throws with refundOnError: falseNoOpt-out — throw propagates, charge stands.
Handler succeeds but upstream returned bad dataNoNot a throw. Inspect the response and throw manually if it signals failure.
Use short snake_case messages like 'upstream_rate_limited' or 'parallel_upstream_error' for your throws. They become both the refund reason (stored on the platform for analytics) and the error field in the 502 response (so clients can branch on them).
Refund failures are logged but don't mask the original error. The caller always gets the 502 with their error code even if the refund call itself fails — on-call engineers can reconcile later. Auto-refunds are best-effort, not transactional.