# 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.

3 steps, 2 API calls

**Products:** 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`

[Reference for this operation](https://devportal-simpay-sbx.winkpg.io/docs/api/transactionsCreate.md)

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.

[Testing your integration](https://devportal-simpay-sbx.winkpg.io/docs/testing.md)

cURL:

```bash
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:

```csharp
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`

[Reference for this operation](https://devportal-simpay-sbx.winkpg.io/docs/api/transactionsCreate.md)

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.

[Testing your integration](https://devportal-simpay-sbx.winkpg.io/docs/testing.md)

cURL:

```bash
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:

```csharp
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.

[Testing your integration](https://devportal-simpay-sbx.winkpg.io/docs/testing.md)

[Retry a payment without a double charge](https://devportal-simpay-sbx.winkpg.io/docs/blueprints/retry-a-payment-safely.md)

## 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.

### cURL

```bash
brew install jq
```

```bash
#!/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.
```

### PowerShell

```powershell
# 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.
```

### TypeScript

```typescript
// 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.
```

### C#

```csharp
// 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.
```

### Python

```bash
pip install requests
```

```python
# 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.
```

- [Blueprints](https://devportal-simpay-sbx.winkpg.io/docs/blueprints.md): every blueprint this instance publishes.

## See also

- [All documentation](https://devportal-simpay-sbx.winkpg.io/llms.txt): the machine-readable index of every public page on this site.
