The sandbox runs the real Subby subscription engine with simulated payment-rail responses, simulated message delivery and controllable time. Everything on this page only works with a sk_test_ key against https://sandbox-api.mysubbyapp.com/v1. None of it is metered.
Test payment methods
In sandbox you can pass these payment method IDs directly, anywhere the API accepts payment_method: when creating a subscription, when updating a customer's default_payment_method, or when retrying a charge. They also appear in the payment method picker on sandbox checkout pages.
| Payment method | Type | Behaviour |
|---|---|---|
pm_test_success |
Card | Every charge succeeds |
pm_test_bank_transfer_success |
Bank account | Every charge succeeds |
pm_test_ussd_success |
USSD | Every charge succeeds after about 10 seconds |
pm_test_renewal_fails |
Card | The first charge succeeds. Every renewal fails with insufficient_funds |
pm_test_recovers_on_retry_1 |
Card | Renewals fail with insufficient_funds, then succeed on the 1st retry |
pm_test_recovers_on_retry_2 |
Card | Renewals fail, then succeed on the 2nd retry |
pm_test_insufficient_funds |
Card | Every charge fails with insufficient_funds (retryable) |
pm_test_do_not_honor |
Card | Every charge fails with do_not_honor (retryable) |
pm_test_expired_card |
Card | Every charge fails with expired_card (not retryable) |
pm_test_authentication_required |
Card | Every charge fails with authentication_required |
pm_test_card_reported_stolen |
Card | Every charge fails with card_reported_stolen (not retryable) |
pm_test_slow_processing |
Card | Charges stay pending for 60 seconds, then succeed |
Real card numbers are rejected in sandbox.
curl "https://sandbox-api.mysubbyapp.com/v1/subscriptions" \
-H "Authorization: Bearer $SUBBY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "customer": "cus_REPLACE_ME", "plan": "plan_REPLACE_ME", "payment_method": "pm_test_renewal_fails" }'
Test phone numbers
Nudges in sandbox are never sent to real people. Use these numbers to control what happens:
| Phone number | SMS | |
|---|---|---|
+2348000000001 |
Delivered | Delivered |
+2348000000002 |
Sent, no delivery receipt | Sent, no delivery receipt |
+2348000000003 |
Failed: unreachable |
Failed: unreachable |
+2348000000004 |
Failed: invalid_number |
Failed: invalid_number |
+2348000000005 |
Delivered | Failed: not_on_whatsapp (tests fallback to the next channel) |
| Any other number | Delivered | Delivered |
Test email addresses
| Result | |
|---|---|
Any address at example.com |
Delivered |
bounce@sandbox.mysubbyapp.com |
Failed: bounced |
complaint@sandbox.mysubbyapp.com |
Delivered, then marked spam_complaint |
Nudge outbox
Every sandbox nudge is stored with the exact message the customer would have received, including the payment link. View it in Dashboard → Developers → Sandbox → Nudge outbox, or:
curl "https://sandbox-api.mysubbyapp.com/v1/test/nudge-outbox?customer=cus_REPLACE_ME" \
-H "Authorization: Bearer $SUBBY_SECRET_KEY"
{
"success": true,
"data": [
{
"id": "ndg_7Yt2Qa",
"object": "nudge",
"channel": "whatsapp",
"status": "delivered",
"to": "+2348000000001",
"template": "payment_failed",
"rendered_message": "Hi Ada, your ₦5,000 payment for Pro Monthly didn't go through. Pay or update your card here: https://pay.sandbox.mysubbyapp.com/n/7Yt2Qa",
"payment_link": "https://pay.sandbox.mysubbyapp.com/n/7Yt2Qa",
"sent_at": "2026-10-02T06:00:11Z"
}
],
"meta": { "has_more": false, "next_cursor": null, "limit": 20 }
}
Open the payment_link to test the customer's recovery flow with any test payment method.
Test clocks
Test clocks let you move time forward so you can test a whole billing cycle in minutes.
1. Create a clock set to a starting time:
curl "https://sandbox-api.mysubbyapp.com/v1/test/clocks" \
-H "Authorization: Bearer $SUBBY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Renewal failure test", "frozen_time": "2026-10-01T09:00:00Z" }'
2. Create customers on the clock. Pass test_clock when creating the customer. Everything for that customer, including subscriptions, charges, retries and nudges, follows the clock's time instead of real time.
curl "https://sandbox-api.mysubbyapp.com/v1/customers" \
-H "Authorization: Bearer $SUBBY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Clock Customer", "phone": "+2348000000001", "test_clock": "clock_REPLACE_ME" }'
3. Advance the clock:
curl -X POST "https://sandbox-api.mysubbyapp.com/v1/test/clocks/clock_REPLACE_ME/advance" \
-H "Authorization: Bearer $SUBBY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "frozen_time": "2026-11-02T09:00:00Z" }'
The clock's status changes to advancing while Subby runs everything that would have happened in that time, in order. When it finishes, the status returns to ready and you receive a test_clock.ready webhook. Events created along the way are sent to your endpoints with their simulated created_at.
Limits:
- Time only moves forward, by up to 1 year per advance.
- Up to 10 clocks per account, each with up to 10 customers.
- Clocks and their customers are deleted 30 days after creation, or with
DELETE /test/clocks/{id}.
Triggering events
Build and test your webhook handler before you have real activity. Subby creates realistic sandbox objects for the event and sends it to your endpoints:
curl "https://sandbox-api.mysubbyapp.com/v1/test/events/trigger" \
-H "Authorization: Bearer $SUBBY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "subscription.escalated", "overrides": { "escalation": { "action": "restrict" } } }'
typeaccepts any event type.- Pass
objectwith an existing sandbox ID (for example"object": "sub_9fK3mQ2xLp") to use your own data instead of generated objects. overrideslets you set fields on the generated object.
Resetting the sandbox
POST /test/reset with { "confirm": "RESET" } deletes all sandbox data except API keys and webhook endpoints. See Environments & sandbox.
Test recipes
A renewal fails, then the customer pays through a nudge
- Create a monthly plan with
retry_logic.retry_intervals_days: [1, 3, 7]andreminders.post_due_days: [1, 3]. - Create a test clock at
2026-10-01T09:00:00Zand a customer on it with phone+2348000000001. - Create a subscription with
payment_method: "pm_test_renewal_fails". Expectsubscription.createdandcharge.succeeded. - Advance the clock to
2026-11-01T10:00:00Z. Expectcharge.failedandsubscription.past_due. - Advance to
2026-11-02T10:00:00Z. Expectnudge.sentandnudge.delivered. Check the nudge outbox. - Open the nudge's
payment_linkand pay withpm_test_success. Expectcustomer.payment_method_updated,charge.succeededandsubscription.recovered.
All retries fail and service is paused
- Use a plan with
escalation_on_failure: "pause_service". - Subscribe a clock customer with
pm_test_insufficient_fundsafter a successful first payment, or usepm_test_renewal_fails. - Advance the clock past the renewal date plus the last retry interval.
- Expect 3
charge.failedevents, thensubscription.escalatedandsubscription.paused. Confirm your system removed access.
A card that can't be retried
- Subscribe a clock customer with
pm_test_renewal_fails, then update theirdefault_payment_methodtopm_test_expired_card. - Advance past the renewal date.
- Expect one
charge.failedwithfailure_code: "expired_card"and nocharge.retry_scheduled. The nudge outbox shows an "update your card" message.
WhatsApp fallback to SMS
- Create a customer with phone
+2348000000005on a plan withreminders.channels: ["whatsapp", "sms"]. - Trigger a failed renewal.
- Expect
nudge.failedwithfailure_reason: "not_on_whatsapp", followed bynudge.sentandnudge.deliveredonsms.