Simulate processor latency
Make the sandbox processor take its time, then make it answer later than your own client is willing to wait, so your timeout path is something you have run rather than something you have written.
Signed in, you can run this blueprint against your own sandbox merchant one call at a time, with the values from each call threaded into the next. Sign in to run it.
3 steps, 2 API callsPaymentsTransactions
Samples use {{API_KEY}} for your API key and {{BASE_URL}} for this instance's API address. Anything else in double braces is a value an earlier step gave you.
Drive each latency profile
1. Send a sale through a degraded processor
API call
POST /api/transactions
Send "loopback.latencyProfile" or "loopbacklatencyprofile" on a transaction custom field, with the value "slow" on it. A degraded processor. Use this to check your own timeouts and retries. Expect about 500 ms at the median, 2000 ms at the 95th percentile, and 4000 ms at the worst, which is where the sampled delay is clamped. The amount is the sandbox's guaranteed approval, so the only thing this run changes is how long the answer takes. The response still arrives and the sale still approves. What changes is how long your own code was holding the request open, which is the part that breaks first when a processor has a bad afternoon: a connection pool sized for a fast answer runs out, and requests that had nothing wrong with them start failing behind it.
Values this step gives you
curl -X POST "{{BASE_URL}}/api/transactions" \
-H "api-key: {{API_KEY}}" \
-H "Content-Type: application/json" \
-d '{
"transactionType": "Sale",
"cardData": {
"cardNumber": "4111111111111111",
"nameOnCard": "Jane Doe",
"expirationMonth": 12,
"expirationYear": 2030,
"cvv": 123
},
"customFields": [
{ "name": "loopback.latencyProfile", "value": "slow" }
],
"invoiceData": {
"amounts": { "base": 10.00, "total": 10.00 }
}
}'using var http = new HttpClient
{
BaseAddress = new Uri("{{BASE_URL}}"),
// Your own budget, not the platform's. Set it to what you ship, then run the
// step above and watch this throw.
Timeout = TimeSpan.FromSeconds(5)
};
http.DefaultRequestHeaders.Add("api-key", "{{API_KEY}}");
var response = await http.PostAsJsonAsync("/api/transactions", new
{
transactionType = "Sale",
cardData = new
{
cardNumber = "4111111111111111",
nameOnCard = "Jane Doe",
expirationMonth = 12,
expirationYear = 2030,
cvv = 123
},
customFields = new[]
{
new { name = "loopback.latencyProfile", value = "slow" }
},
invoiceData = new
{
amounts = new { @base = 10.00m, total = 10.00m }
}
});
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
var resultCode = result.GetProperty("responseData").GetProperty("resultCode").GetString();2. Send a sale that outlasts a typical client timeout
API call
POST /api/transactions
Send "loopback.latencyProfile" or "loopbacklatencyprofile" on a transaction custom field, with the value "timeout" on it. Long enough to trip most client timeouts. Use this to exercise your timeout path. Expect about 3000 ms at the median, 10000 ms at the 95th percentile, and 15000 ms at the worst, which is where the sampled delay is clamped. The amount is the sandbox's guaranteed approval, so the only thing this run changes is how long the answer takes. Long enough that most HTTP clients give up first. Giving up isn't the same as the payment not happening: the request is still in flight, and it may well approve after your client has stopped listening. This is the outcome you can't tell apart from a failure without asking, and asking is what the safe-retry blueprint below is about.
Values this step gives you
curl -X POST "{{BASE_URL}}/api/transactions" \
-H "api-key: {{API_KEY}}" \
-H "Content-Type: application/json" \
-d '{
"transactionType": "Sale",
"cardData": {
"cardNumber": "4111111111111111",
"nameOnCard": "Jane Doe",
"expirationMonth": 12,
"expirationYear": 2030,
"cvv": 123
},
"customFields": [
{ "name": "loopback.latencyProfile", "value": "timeout" }
],
"invoiceData": {
"amounts": { "base": 10.00, "total": 10.00 }
}
}'using var http = new HttpClient
{
BaseAddress = new Uri("{{BASE_URL}}"),
// Your own budget, not the platform's. Set it to what you ship, then run the
// step above and watch this throw.
Timeout = TimeSpan.FromSeconds(5)
};
http.DefaultRequestHeaders.Add("api-key", "{{API_KEY}}");
var response = await http.PostAsJsonAsync("/api/transactions", new
{
transactionType = "Sale",
cardData = new
{
cardNumber = "4111111111111111",
nameOnCard = "Jane Doe",
expirationMonth = 12,
expirationYear = 2030,
cvv = 123
},
customFields = new[]
{
new { name = "loopback.latencyProfile", value = "timeout" }
},
invoiceData = new
{
amounts = new { @base = 10.00m, total = 10.00m }
}
});
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
var resultCode = result.GetProperty("responseData").GetProperty("resultCode").GetString();Decide what your client does about it
3. Set your own timeout, then decide what happens when it fires
On your side
A timeout is a decision about how long you are willing to wait, not a report that nothing happened. Pick one on purpose, log the request you abandoned, and never resend a payment blind: ask what became of it first. Send "loopback.latencyMs" instead of a named profile to hold the answer for an exact number of milliseconds, which is how you pin a run to the boundary your own client sits on. Both fields ride on a transaction custom field. Every profile "loopback.latencyProfile" accepts is listed on the testing page. One constraint applies to the custom-field channel. If the merchant has defined any custom fields at all, every submitted custom-field name has to match one of those definitions, and a name that doesn't match is rejected with a 400. A sandbox merchant with no custom-field definitions accepts any name, which is the usual case. If you get a 400 naming the control field you sent, define a custom field with that name on the merchant.
Retry a payment without a double charge