subbydocs
Visit Subby Start building

Testing in sandbox

Test payment methods, test phone numbers, test clocks, triggered events, the nudge outbox and step-by-step test recipes.

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 WhatsApp
+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

Email 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" } } }'
  • type accepts any event type.
  • Pass object with an existing sandbox ID (for example "object": "sub_9fK3mQ2xLp") to use your own data instead of generated objects.
  • overrides lets 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

  1. Create a monthly plan with retry_logic.retry_intervals_days: [1, 3, 7] and reminders.post_due_days: [1, 3].
  2. Create a test clock at 2026-10-01T09:00:00Z and a customer on it with phone +2348000000001.
  3. Create a subscription with payment_method: "pm_test_renewal_fails". Expect subscription.created and charge.succeeded.
  4. Advance the clock to 2026-11-01T10:00:00Z. Expect charge.failed and subscription.past_due.
  5. Advance to 2026-11-02T10:00:00Z. Expect nudge.sent and nudge.delivered. Check the nudge outbox.
  6. Open the nudge's payment_link and pay with pm_test_success. Expect customer.payment_method_updated, charge.succeeded and subscription.recovered.

All retries fail and service is paused

  1. Use a plan with escalation_on_failure: "pause_service".
  2. Subscribe a clock customer with pm_test_insufficient_funds after a successful first payment, or use pm_test_renewal_fails.
  3. Advance the clock past the renewal date plus the last retry interval.
  4. Expect 3 charge.failed events, then subscription.escalated and subscription.paused. Confirm your system removed access.

A card that can't be retried

  1. Subscribe a clock customer with pm_test_renewal_fails, then update their default_payment_method to pm_test_expired_card.
  2. Advance past the renewal date.
  3. Expect one charge.failed with failure_code: "expired_card" and no charge.retry_scheduled. The nudge outbox shows an "update your card" message.

WhatsApp fallback to SMS

  1. Create a customer with phone +2348000000005 on a plan with reminders.channels: ["whatsapp", "sms"].
  2. Trigger a failed renewal.
  3. Expect nudge.failed with failure_reason: "not_on_whatsapp", followed by nudge.sent and nudge.delivered on sms.