Testing
Test mode runs the whole flow, from order to webhook to refund, without moving money. Use it for every scenario your code has to handle.
Test mode
- Use a key that starts with
vp_sk_test_. Everything created with it is a test object (livemode: false) and stays separate from live data. - Test mode works before your business is verified and offers every payment method, whatever is switched on for live mode.
- The same validation applies as in live mode: amount limits per method, currencies, references unique per mode, idempotency.
- Webhook endpoints are per mode. Add a test-mode endpoint to receive test events; see webhooks for testing through a tunnel.
- Nothing is ever sent on a chain, to Cash App or to a card provider.
Simulate a payment
Open the pay_link of a test order and choose a method. The checkout shows a Test controls box:
| Button | Methods | Result |
|---|---|---|
| Simulate payment | crypto, Cash App, card | The order becomes paid; you receive order.paid. |
| Simulate half payment | crypto | Half the amount arrives. The order becomes underpaid with amount_paid at half; you receive order.underpaid. |
| Simulate expiry | any, while nothing is paid | The order ends as if its time ran out; you receive order.expired, the same event the expiry job sends in live mode. |
A crypto test payment shows a fake transaction hash starting with test_. After the simulation the payer is offered the way back to your success_url, so you can test your return page too.
Simulate the other outcomes
| Scenario | How | Event |
|---|---|---|
| Payer never pays | Click Simulate expiry, or create the order with "expires_in": 900 (the minimum, 15 minutes) and leave it. | order.expired |
| You cancel the order | POST /api/v1/orders/:id/void while it is unpaid. | order.voided |
| Payer cancels | Click the cancel link on the checkout. | None. The payer lands on cancel_url with velpay_status=canceled; the order stays open until it expires. |
| Refund | POST /api/v1/orders/:id/refunds on a paid test order, with any well-formed address as destination. | refund.created, then refund.sent right away (no review in test mode) |
| Method not available | An amount below the method's minimum, e.g. "methods": ["card"] with "amount": 500 USD. | None; the API answers 400 amount_too_small (see errors) |
| Retried request | Send the same POST /orders twice with one Idempotency-Key. | One order; the second answer has Idempotent-Replay: 1 |
Checklist before going live
- A paid order is fulfilled exactly once, even when the same event is delivered twice.
- Your code rejects a webhook with a wrong signature (change one byte of the body) and one older than five minutes.
- An underpaid order is not fulfilled automatically.
- An expired or voided order releases stock or reservations.
- Your return page doesn't mark anything paid by itself; it shows "payment being confirmed" until the webhook arrives.
- An API error (for example a key typo) shows the customer a clear message and gets logged with the
Request-Id. - You have added the live webhook endpoint and swapped in the live key. See going live.