No such blueprint
This instance publishes no blueprint at that address. The catalog lists every one it does publish.
Back to the blueprints
Blueprints
/
Testing scenarios
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 .
On this page
3 steps, 2 API calls Payments Transactions
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.
Reference for this operation
Testing your integration
Values this step gives you
cURL CopycURL
Copied
Copy failed
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 }
}
}'.NET Copy.NET
Copied
Copy failed
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.
Reference for this operation
Testing your integration
Values this step gives you
cURL CopycURL
Copied
Copy failed
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 }
}
}'.NET Copy.NET
Copied
Copy failed
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.
Reference for this operation
Testing your integration
Retry a payment without a double charge
Values this step gives you
Run the whole flow as one script
Every step above in one script you can copy and run. Replace {{API_KEY}} with your own API key and {{BASE_URL}} with this instance's API address, and set any value the script asks you for at the top. A step that happens outside the API stays a comment.
Code sample language >_
cURL PS
PowerShell TS
TypeScript C#
C# Py
Python
brew install jq
Copythe install command for cURL
Copied
Copy failed
cURL CopycURL
Copied
Copy failed
#!/usr/bin/env bash
# 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.
#
# Every API call in this blueprint, in order. Each value a call returns is passed to the calls after
# it. A step that happens outside the API is a comment, and any failed call stops the script.
set -euo pipefail
BASE_URL="{{BASE_URL}}"
API_KEY="{{API_KEY}}"
# Sends one request and prints the response body. A failed call prints the API's answer and stops
# the script.
call() {
local method="$1" path="$2" body="${3:-}" out
local args=(-sS --fail-with-body -X "$method" "$BASE_URL$path" -H "api-key: $API_KEY")
if [ -n "$body" ]; then
args+=(-H "Content-Type: application/json" -d "$body")
fi
if ! out=$(curl "${args[@]}"); then
printf '%s\n' "$out" >&2
return 1
fi
printf '%s' "$out"
}
# Phase 1: Drive each latency profile
# Step 1: Send a sale through a degraded processor
call POST "/api/transactions" '{
"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 }
}
}' > /dev/null
# Step 2: Send a sale that outlasts a typical client timeout
call POST "/api/transactions" '{
"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 }
}
}' > /dev/null
# Phase 2: Decide what your client does about it
# Step 3: Set your own timeout, then decide what happens when it fires
# 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.
Copythe install command for PowerShell
Copied
Copy failed
PowerShell CopyPowerShell
Copied
Copy failed
# 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.
#
# Every API call in this blueprint, in order. Each value a call returns is passed to the calls after
# it. A step that happens outside the API is a comment, and any failed call stops the script.
$ErrorActionPreference = 'Stop'
$baseUrl = '{{BASE_URL}}'
$apiKey = '{{API_KEY}}'
# Sends one request and returns the parsed response body. A failed call stops the script.
function Invoke-BlueprintCall([string] $Method, [string] $Path, [string] $Body) {
$arguments = @{
Method = $Method
Uri = $baseUrl + $Path
Headers = @{ 'api-key' = $apiKey }
}
if ($Body) {
$arguments.ContentType = 'application/json'
$arguments.Body = [System.Text.Encoding]::UTF8.GetBytes($Body)
}
Invoke-RestMethod @arguments
}
# Phase 1: Drive each latency profile
# Step 1: Send a sale through a degraded processor
$body = @'
{
"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 }
}
}
'@
$null = Invoke-BlueprintCall -Method 'POST' -Path '/api/transactions' -Body $body
# Step 2: Send a sale that outlasts a typical client timeout
$body = @'
{
"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 }
}
}
'@
$null = Invoke-BlueprintCall -Method 'POST' -Path '/api/transactions' -Body $body
# Phase 2: Decide what your client does about it
# Step 3: Set your own timeout, then decide what happens when it fires
# 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.
Copythe install command for TypeScript
Copied
Copy failed
TypeScript CopyTypeScript
Copied
Copy failed
// 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.
//
// Every API call in this blueprint, in order. Each value a call returns is passed to the calls
// after it. A step that happens outside the API is a comment, and any failed call stops the script.
export {};
const baseUrl = '{{BASE_URL}}';
const apiKey = '{{API_KEY}}';
// Sends one request and returns the parsed response body. A failed call throws.
async function call(method: string, path: string, body?: unknown): Promise<any> {
const headers: Record<string, string> = { 'api-key': apiKey };
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(baseUrl + path, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`${method} ${path} answered ${response.status}: ${text}`);
}
return text ? JSON.parse(text) : null;
}
// Phase 1: Drive each latency profile
// Step 1: Send a sale through a degraded processor
await call('POST', '/api/transactions', {
"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 }
}
});
// Step 2: Send a sale that outlasts a typical client timeout
await call('POST', '/api/transactions', {
"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 }
}
});
// Phase 2: Decide what your client does about it
// Step 3: Set your own timeout, then decide what happens when it fires
// 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.
Copythe install command for C#
Copied
Copy failed
C# CopyC#
Copied
Copy failed
// 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.
//
// Every API call in this blueprint, in order. Each value a call returns is passed to the calls
// after it. A step that happens outside the API is a comment, and any failed call stops the script.
using System.Text;
using System.Text.Json;
var baseUrl = "{{BASE_URL}}";
var apiKey = "{{API_KEY}}";
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("api-key", apiKey);
// Sends one request and returns the parsed response body. A failed call throws.
async Task<JsonElement> CallAsync(string method, string path, string? body = null)
{
using var request = new HttpRequestMessage(new HttpMethod(method), baseUrl + path);
if (body is not null)
{
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await http.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException($"{method} {path} answered {(int)response.StatusCode}: {json}");
}
return json.Length == 0 ? default : JsonSerializer.Deserialize<JsonElement>(json);
}
// Phase 1: Drive each latency profile
// Step 1: Send a sale through a degraded processor
await CallAsync("POST", "/api/transactions", """
{
"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 }
}
}
""");
// Step 2: Send a sale that outlasts a typical client timeout
await CallAsync("POST", "/api/transactions", """
{
"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 }
}
}
""");
// Phase 2: Decide what your client does about it
// Step 3: Set your own timeout, then decide what happens when it fires
// 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.
pip install requests
Copythe install command for Python
Copied
Copy failed
Python CopyPython
Copied
Copy failed
# 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.
#
# Every API call in this blueprint, in order. Each value a call returns is passed to the calls after
# it. A step that happens outside the API is a comment, and any failed call stops the script.
import requests
BASE_URL = "{{BASE_URL}}"
API_KEY = "{{API_KEY}}"
# Sends one request and returns the parsed response body. A failed call raises.
def call(method, path, body=None):
response = requests.request(
method,
BASE_URL + path,
headers={"api-key": API_KEY},
json=body,
)
response.raise_for_status()
return response.json() if response.content else None
# Phase 1: Drive each latency profile
# Step 1: Send a sale through a degraded processor
call("POST", "/api/transactions", {
"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 }
}
})
# Step 2: Send a sale that outlasts a typical client timeout
call("POST", "/api/transactions", {
"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 }
}
})
# Phase 2: Decide what your client does about it
# Step 3: Set your own timeout, then decide what happens when it fires
# 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.
Next steps