Testing Declines
Guide to testing sandbox decline, provider error, retry/fallback, and the normalized error model.
Integration quality shows not in successful payments, but in what you do when declined. In sandbox, deliberately trigger decline and timeout to map UI, webhooks, and state before production.
Finish these scenarios before going live so you do not panic on the first real hard decline in production.
Why Test Declines?
Testing only successful payments is not enough. Real product quality is measured by correctly handling invalid card, insufficient funds, issuer unavailable, provider timeout, 3DS failed, suspected fraud, duplicate request, expired card, hard decline, soft decline, and retryable/non-retryable error behavior.
HSRC Pay's normalized error model makes provider chaos readable for merchants.
Hard Decline vs Soft Decline
| Decline type | Meaning | Merchant behavior |
|---|---|---|
| Hard decline | Retrying with the same payment method is not appropriate | Do not retry; ask the user for a different payment method |
| Soft decline | Transient condition or error that may resolve with a different route | Fallback/retry, status check, or try again later if appropriate |
| Unknown | Result is uncertain or provider response is ambiguous | Proceed carefully with idempotency and status query |
Error Codes (Public Summary)
In the confirm orchestrator summary, errCode comes as a string; for declines there is also a hard / soft distinction. Common codes:
| Code | Usually |
|---|---|
invalid_payment_method | Card/method invalid (hard) |
invalid_currency | Currency mismatch (hard) |
invalid_credentials / invalid_api_key | Provider configuration (hard) |
system_proxy_timeout | Transient; status check (soft) |
system_network_other / system_network_econnreset | Transient network (soft) |
system_no_provider_strategy_available_for_routing_plan | Routing/config (hard) |
system_unknown | Ambiguous; proceed carefully |
Do not show raw provider messages to users; simplify based on errCode and decline type.
Expected Error Simulation in Sandbox
Sandbox scenario configuration can produce specific outcomes. If your integration exposes this field under a different name, the same mental model applies: explicit sandbox configuration triggers a provider or normalized error result; sandbox card numbers do not.
{
"amount": 12500,
"currency": "TRY",
"payment_method": "pm_sandbox_card",
"sandbox": {
"expectedErr": "system_unknown"
}
}{
"kind": "declined",
"errCode": "system_unknown",
"errMsg": "Felix Sandbox: Simulation of system_unknown",
"type": "hard",
"providerRef": "sandbox_provider_ref",
"traceId": "trace_sandbox_123"
}Card Validation Decline
Sandbox cards do not simulate specific decline reasons. A sandbox card number does not mean insufficient funds, expired card, invalid account, stolen card, issuer unavailable, or any other issuer-specific response.
The only card-list decline behavior is validation: if a card payment method does not match the sandbox card allowlist, the sandbox provider returns invalid_payment_method with Invalid sandbox test card.
Use expectedSandboxErr or provider-specific sandbox configuration for explicit error simulation, not card numbers.
Provider-Based Decline Scenarios
Provider simulation can test:
- Provider timeout
- Provider maintenance/unavailable
- Unsupported operation
- Invalid credential configuration
- Mapping failure or unknown provider result
The merchant must not rely on raw provider errors; behave according to the normalized error model.
3DS Failure Scenarios
If a 3DS challenge is cancelled, expires, or completes unsuccessfully, the result may be decline. In this case, give the user a path to restart the challenge or choose a different payment method.
Retry and Fallback
- Hard: Request a new payment method; do not auto-loop with the same card.
- Soft: Next provider candidate or controlled re-confirm; first
GET /payments/:id?include_charges=true. - Ambiguous timeout: Check the existing record and webhook logs before opening a new payment.
Webhook/Event Handling
Multiple events may arrive during decline or retry. The merchant webhook handler:
- Must be idempotent.
- Must interpret payment state transitions in order.
- Must perform status check on unknown/timeout.
- Must produce UI messages from normalized error, not raw provider strings.
Example Test Matrix
| Scenario | Normalized Error | Decline Type | Retry/Fallback | Merchant UI Message | Webhook Expectation |
|---|---|---|---|---|---|
| Invalid card/payment method | invalid_payment_method | hard | No retry | Check card details | payment.requires_payment_method / charge declined |
| Stolen payment method | stolen_payment_method | hard | No retry | Request different payment method | charge declined |
| Invalid currency | invalid_currency | hard | Fix configuration | Currency not supported | failed/declined event |
| Invalid API key | invalid_api_key | hard | Fix provider config | Provider connection could not be configured | failed event |
| Invalid credentials | invalid_credentials | hard | Fix provider config | Provider credentials invalid | failed event |
| Proxy connection failed | system_proxy_conn_failed | soft | Fallback/retry may be tried | Provider connection could not be established | retry/fallback trace |
| Proxy auth failed | system_proxy_auth_failed | hard | Fix proxy config | Provider connection could not be configured | failed event |
| Proxy timeout | system_proxy_timeout | soft | Status check | Transaction is being verified | payment.processing or update |
| Network other | system_network_other | soft | Fallback/retry may be tried | Temporary network issue | retry/fallback trace |
| Network ECONNRESET | system_network_econnreset | soft | Fallback/retry may be tried | Temporary network issue | retry/fallback trace |
| Max retry exceeded | system_candidate_processor_max_retry_exceeded | soft | New route/status check | Retry attempts completed | failed/updated event |
| No provider config for plan | system_no_provider_strategy_available_for_routing_plan | hard | Fix routing/provider config | No suitable provider found (API error code; not a product step called "strategy execution") | failed event |
| System unknown | system_unknown | soft or hard | Careful retry/status check | Transaction status is being verified | updated or failed |
Best Practices
- Test success, hard decline, soft decline, timeout, and 3DS failed scenarios separately.
- Base retry decisions on
typeand normalized error, not raw messages. - Use idempotency keys.
- Make webhook handler resilient to duplicate delivery.
- Trace fallback attempts.
- Separate user messages from technical provider messages.
Common Mistakes
- Showing the same UI message for every decline result.
- Auto-retrying with the same card after hard decline.
- Creating a new payment immediately after timeout, assuming payment failed.
- Treating webhook duplicate delivery as an error.
- Not testing production provider error mapping in sandbox.