Test helpers
Test helper endpoints
Vatly provides a small set of test helper endpoints for recurring billing scenarios. These endpoints are only available in test mode.
test_ API token for every endpoint on this page. Test helper endpoints are not available with live credentials.Fast-forward subscription renewal
POST /v1/test-helpers/subscriptions/{subscriptionId}/fast-forward-renewal
Simulate a renewal cycle for an existing subscription. The subscription must be active and have a scheduled next renewal.
This helper is asynchronous, just like a live renewal. The request is validated straight away, but the renewal itself runs from a queue a few seconds later — so the response returns the subscription as it was before the renewal: renewedAt, renewedUntil, and nextRenewalAt have not moved yet. Follow the order.paid and order.payment_failed webhooks for the outcome, exactly as your live integration should.
Useful for:
- testing renewal billing flows without waiting for the real billing interval
- verifying subscription lifecycle events and webhook delivery
- validating dunning or invoice follow-up automation in your sandbox flow
- forcing the renewal payment to fail so you can exercise your payment-recovery handling
Request body
The request body is optional. Omit it to advance the billing cycle and leave the renewal payment pending (this endpoint's original behaviour).
| Attribute | Type | Description |
|---|---|---|
paymentStatus | string | Optional. Outcome to force on the renewal payment. One of paid or failed. Omit to leave the renewal payment pending. failed declines the payment and starts a payment recovery for the renewal order (delivering order.payment_failed to your webhook endpoint); paid settles it. |
failureReason | string | Optional. Which decline to simulate. Only valid alongside paymentStatus: failed (sending it with paid is rejected). Soft declines (insufficient_funds, temporary_decline, general_failure) keep the payment method and retry on a multi-week timeline; every other value is a hard decline that drives the customer to supply a new payment method on a short timeline. Defaults to general_failure. Allowed values: insufficient_funds, temporary_decline, general_failure, invalid_mandate, mandate_canceled, account_closed, card_expired, card_lost_or_stolen, invalid_card_details, authentication_failed, fraud_suspected. |
paymentStatus is provided it is the deterministic outcome; when it is omitted the payment stays pending, and you can settle or decline it later with Simulate an order payment. A requested failed outcome can start payment recovery even before a payment exists, but paid cannot be applied until there is a chargeable payment. If the payment mandate is not active yet, the cycle still advances. Only one unresolved fast-forward per subscription may be in flight at a time — a second request returns 409 while the first is still queued, running, or awaiting recovery.curl -X POST https://api.vatly.com/v1/test-helpers/subscriptions/subscription_Lp3mNvBxKw7RjTgYcZaE/fast-forward-renewal \
-H "Authorization: Bearer test_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"paymentStatus": "failed",
"failureReason": "card_expired"
}'
$vatly = new \Vatly\API\VatlyApiClient();
$vatly->setApiKey('test_your_api_key_here');
$subscription = $vatly->testHelpers->fastForwardRenewal('subscription_Lp3mNvBxKw7RjTgYcZaE', [
'paymentStatus' => 'failed',
'failureReason' => 'card_expired',
]);
{
"id": "subscription_Lp3mNvBxKw7RjTgYcZaE",
"resource": "subscription",
"customerId": "customer_Lp3mNvBxKw7RjTgYcZaE",
"subscriptionPlanId": "subscription_plan_Rk5pQrSvWm8NjLhYbUcP",
"testmode": true,
"name": "Pro Monthly",
"description": "Full access to all Pro features",
"billingAddress": {
"fullName": "John Doe",
"companyName": "Acme Corp",
"streetAndNumber": "123 Main Street",
"city": "Berlin",
"postalCode": "10115",
"country": "DE"
},
"basePrice": {
"value": "29.00",
"currency": "EUR"
},
"quantity": 1,
"interval": "month",
"intervalCount": 1,
"status": "active",
"cancellationReason": null,
"mandate": {
"method": "card",
"maskedIdentifier": "4242"
},
"startedAt": "2024-01-15T10:30:00Z",
"endedAt": null,
"canceledAt": null,
"renewedAt": null,
"renewedUntil": null,
"nextRenewalAt": "2024-02-15T10:30:00Z",
"trialUntil": null,
"scheduledUpdate": null,
"links": {
"self": {
"href": "https://api.vatly.com/v1/subscriptions/subscription_Lp3mNvBxKw7RjTgYcZaE",
"type": "application/json"
},
"customer": {
"href": "https://api.vatly.com/v1/customers/customer_Lp3mNvBxKw7RjTgYcZaE",
"type": "application/json"
}
}
}
Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid API key |
403 | Endpoint not available for this token or resource |
404 | Subscription not found |
409 | A fast-forward for this subscription is still queued, running, or awaiting recovery |
422 | Invalid request body (for example, failureReason sent with paymentStatus: paid) |
Simulate an order payment
POST /v1/test-helpers/orders/{orderId}/simulate-payment
Settle or decline the pending payment of a test order that was charged to the customer's saved payment method — for example a subscription renewal, or a subscription update made with invoiceImmediately: true.
Use this when a test-mode payment on a saved payment method is still pending. Test card payments settle as paid right away. Test SEPA Direct Debit payments settle as paid automatically 10 to 15 minutes after they are created, unless you simulate an outcome first — simulate failed within the first 10 minutes to test your payment-recovery handling. Payments created locally by the fast-forward and payment-recovery helpers are never settled automatically; they stay pending until you simulate an outcome here.
Unlike fast-forward renewal, this does not advance the billing cycle — it only resolves a payment that is already awaiting an outcome.
This helper is asynchronous, just like a live payment. The request is validated straight away, but the outcome is applied from a queue a few seconds later, so the returned order is still awaiting payment. Follow the order.paid / order.payment_failed webhooks for the result.
Request body
| Attribute | Type | Description |
|---|---|---|
paymentStatus | string | Required. Outcome to force on the order's pending payment. One of paid or failed. paid settles it (delivering order.paid); failed declines it and starts a payment recovery (delivering order.payment_failed). |
failureReason | string | Optional. Which decline to simulate. Only valid alongside paymentStatus: failed (sending it with paid is rejected). Soft declines (insufficient_funds, temporary_decline, general_failure) keep the payment method and retry on a multi-week timeline; every other value is a hard decline that drives the customer to supply a new payment method on a short timeline. Defaults to general_failure. Allowed values: insufficient_funds, temporary_decline, general_failure, invalid_mandate, mandate_canceled, account_closed, card_expired, card_lost_or_stolen, invalid_card_details, authentication_failed, fraud_suspected. |
failed still starts a payment recovery, but paid is rejected. Only one simulation per order can be queued at a time — a second request while one is queued returns 409.curl -X POST https://api.vatly.com/v1/test-helpers/orders/order_Hn5xWqVfKm8RjTgYbUcP/simulate-payment \
-H "Authorization: Bearer test_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"paymentStatus": "paid"
}'
$vatly = new \Vatly\API\VatlyApiClient();
$vatly->setApiKey('test_your_api_key_here');
$order = $vatly->testHelpers->simulateOrderPayment('order_Hn5xWqVfKm8RjTgYbUcP', [
'paymentStatus' => 'paid',
]);
{
"id": "order_Hn5xWqVfKm8RjTgYbUcP",
"resource": "order",
"customerId": "customer_Xk9pQrSvWm4NjLhYbUcP",
"testmode": true,
"metadata": {},
"paymentMethod": null,
"createdAt": "2024-01-15T10:30:00Z",
"status": "pending",
"invoiceNumber": null,
"total": {
"value": "35.09",
"currency": "EUR"
},
"subtotal": {
"value": "29.00",
"currency": "EUR"
},
"reversedSubtotal": {
"value": "0.00",
"currency": "EUR"
},
"refundableSubtotal": {
"value": "29.00",
"currency": "EUR"
},
"taxSummary": [
{
"taxRate": {
"name": "VAT",
"percentage": 21,
"taxablePercentage": 100
},
"amount": {
"value": "6.09",
"currency": "EUR"
}
}
],
"lines": [
{
"id": "order_item_Jk4pQrSvWm8NjLhYbUcP",
"resource": "orderline",
"description": "Pro Monthly Subscription",
"quantity": 1,
"basePrice": {
"value": "29.00",
"currency": "EUR"
},
"total": {
"value": "35.09",
"currency": "EUR"
},
"subtotal": {
"value": "29.00",
"currency": "EUR"
},
"taxes": [
{
"taxRate": {
"name": "VAT",
"percentage": 21,
"taxablePercentage": 100
},
"amount": {
"value": "6.09",
"currency": "EUR"
}
}
],
"productType": "subscription",
"productId": "subscription_Hn5xWqVfKm8RjTgYbUcP"
}
],
"customerDetails": {
"fullName": "John Doe",
"companyName": "Acme Corp",
"streetAndNumber": "123 Main Street",
"city": "Berlin",
"postalCode": "10115",
"country": "DE",
"email": "john@acme.com"
},
"links": {
"self": {
"href": "https://api.vatly.com/v1/orders/order_Hn5xWqVfKm8RjTgYbUcP",
"type": "application/json"
},
"customer": {
"href": "https://api.vatly.com/v1/customers/customer_Xk9pQrSvWm4NjLhYbUcP",
"type": "application/json"
},
"customerInvoice": {
"href": "https://vatly.com/invoices/order_Hn5xWqVfKm8RjTgYbUcP",
"type": "text/html"
}
}
}
Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid API key |
403 | Endpoint not available for this token or resource |
404 | Order not found |
409 | A simulation for this order is already queued |
422 | Invalid request body (for example, failureReason sent with paymentStatus: paid, or paid requested before a chargeable payment exists) |