View as Markdown

llms.txt

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.

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.

Reference for this operation

Values this step gives you

    cURL

    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

    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

    Values this step gives you

      cURL

      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

      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

      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

        brew install jq

        cURL

        #!/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

        # 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

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

        // 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

        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.
        

        Reconnecting to the server

        Could not reconnect

        This session has ended

        Attempt 1

        Your work on this page is still here. Retrying keeps it; reloading starts the page again.

        The server no longer holds this page's state, so it has to be loaded again.