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 Full integrations

Retry a payment without a double charge

Send a payment under a key you chose, ask the platform what became of it, and retry a request you never got an answer to without charging the cardholder twice.

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.

7 steps, 4 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.

Send a payment under a key you chose

1. Choose a key for this payment

On your side

Generate a key your own system can reproduce for this one payment and no other. A UUID is fine, and so is an order identifier, as long as one logical request gets one key. Generate it before you send, not after: a key you make up while retrying is a different key, and a different key is a second charge. Use a fresh one each time you work through this page.

Reference for this operation

Values this step gives you

  • {{idempotencyKey}} The key this payment is sent under. You choose it, so nothing reads it off a response.

2. Send the sale with the key attached

API call

POST /api/transactions

Send an ordinary sale with idempotencyKey alongside it, at the sandbox's guaranteed approval amount. Read idempotencyStatus off the response before you go further. It reports what the key bought on this merchant: KeyAccepted means deduplication is on and this request claimed the key, so a repeat send inside the window replays this transaction instead of charging again. KeyIgnored means deduplication is off for the merchant, so the key is stored and available to look up but a repeat send charges again. Both are ordinary configurations, and the recovery below is correct under either.

Reference for this operation

Values this step gives you

  • {{transactionId}} The id of the sale, from the response body's id property.
  • {{merchantId}} The merchant the sale belongs to, from the response body's merchantId property. The lookup below is merchant-scoped, and a key means nothing outside the merchant it was sent to.
cURL

curl -X POST "{{BASE_URL}}/api/transactions" \
  -H "api-key: {{API_KEY}}" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionType": "Sale",
    "idempotencyKey": "{{idempotencyKey}}",
    "cardData": {
      "cardNumber": "4111111111111111",
      "nameOnCard": "Jane Doe",
      "expirationMonth": 12,
      "expirationYear": 2030,
      "cvv": 123
    },
    "invoiceData": {
      "amounts": { "base": 10.00, "total": 10.00 }
    }
  }'
.NET

using var http = new HttpClient { BaseAddress = new Uri("{{BASE_URL}}") };
http.DefaultRequestHeaders.Add("api-key", "{{API_KEY}}");

// Chosen before the send, and kept, so a retry can reuse this exact value.
var idempotencyKey = "{{idempotencyKey}}";

var response = await http.PostAsJsonAsync("/api/transactions", new
{
    transactionType = "Sale",
    idempotencyKey,
    cardData = new
    {
        cardNumber = "4111111111111111",
        nameOnCard = "Jane Doe",
        expirationMonth = 12,
        expirationYear = 2030,
        cvv = 123
    },
    invoiceData = new
    {
        amounts = new { @base = 10.00m, total = 10.00m }
    }
});

response.EnsureSuccessStatusCode();

var sale = await response.Content.ReadFromJsonAsync<JsonElement>();
var transactionId = sale.GetProperty("id").GetString();
var merchantId = sale.GetProperty("merchantId").GetString();
var idempotencyStatus = sale.GetProperty("idempotencyStatus").GetString();

What this step answers with

Abridged to the properties this step depends on. A real response carries more.

HTTP 200

{
  "id": "9f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "merchantId": "3a7b1c9d-2e4f-4a6b-8c8d-9e0f1a2b3c4d",
  "transactionType": "Sale",
  "idempotencyStatus": "KeyAccepted",
  "resultCode": "Ok",
  "authorizedAmount": 10.00,
  "responseData": {
    "resultCode": "Ok",
    "resultMessage": "Approved"
  }
}

Ask what became of that key

3. Look the key up and see it answer the same transaction

API call

POST /api/transactions/get-by-idempotency-key-async?merchantId={{merchantId}}&idempotencyKey={{idempotencyKey}}

Ask the platform what it did with that key. It answers with the transaction above, id and all, which is the whole recovery mechanism: a key you kept is a question you can still ask after your own process restarted. Run it now, while you know the answer, so you recognise it later when you don't. A key the merchant has never seen answers 404 instead, and that answer alone means nothing was charged.

Reference for this operation

Values this step gives you

    cURL

    # -G moves the --data-urlencode values into the query string and encodes each
    # one, and -X POST keeps the method. A key you chose may carry a character that
    # means something in a URL, and an unencoded one asks about a different key.
    curl -X POST -G "{{BASE_URL}}/api/transactions/get-by-idempotency-key-async" \
      --data-urlencode "merchantId={{merchantId}}" \
      --data-urlencode "idempotencyKey={{idempotencyKey}}" \
      -H "api-key: {{API_KEY}}"
    .NET

    var lookup = await http.PostAsync(
        $"/api/transactions/get-by-idempotency-key-async"
        + $"?merchantId={merchantId}&idempotencyKey={Uri.EscapeDataString(idempotencyKey)}",
        content: null);
    
    if (lookup.StatusCode == HttpStatusCode.NotFound)
    {
        // Nothing was charged under this key. This is the only answer that licenses
        // a resend.
    }
    else
    {
        lookup.EnsureSuccessStatusCode();
    
        var existing = await lookup.Content.ReadFromJsonAsync<JsonElement>();
        var existingId = existing.GetProperty("id").GetString();
    }

    What this step answers with

    Abridged to the properties this step depends on. A real response carries more.

    HTTP 200

    {
      "id": "{{transactionId}}",
      "merchantId": "{{merchantId}}",
      "transactionType": "Sale",
      "idempotencyStatus": "Replayed",
      "resultCode": "Ok",
      "authorizedAmount": 10.00,
      "responseData": {
        "resultCode": "Ok",
        "resultMessage": "Approved"
      }
    }

    Lose the answer, then recover from it

    4. Choose a key for the payment you are about to lose

    On your side

    A second key, for a second payment. Reusing the first one here would be the mistake the closing note is about: the key identifies a request, not a caller, and pointing it at a different payload asks the platform a question that has two answers. Keep this one too. The next step is written to make you glad you did.

    Reference for this operation

    Values this step gives you

    • {{retryKey}} The key the slow payment is sent under. Yours to choose, and different from the first.

    5. Send a payment through a degraded processor

    API call

    POST /api/transactions

    The same sale under the new key, with one extra custom field. Set "loopback.latencyProfile" to the value "slow" and the sandbox processor answers like a degraded one. This run still completes, and it's meant to. What it shows you is the shape of the problem: a request still in flight after you have stopped being sure of it. To lose the answer outright, send "timeout" instead and drive it from your own client with your own timeout set, rather than from this 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

      cURL

      curl -X POST "{{BASE_URL}}/api/transactions" \
        -H "api-key: {{API_KEY}}" \
        -H "Content-Type: application/json" \
        -d '{
          "transactionType": "Sale",
          "idempotencyKey": "{{retryKey}}",
          "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

      var retryKey = "{{retryKey}}";
      
      try
      {
          var slow = await http.PostAsJsonAsync("/api/transactions", new
          {
              transactionType = "Sale",
              idempotencyKey = retryKey,
              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 }
              }
          });
      
          slow.EnsureSuccessStatusCode();
      }
      catch (TaskCanceledException)
      {
          // Your client gave up. The request may still have completed, so the next step
          // is the probe, never a resend.
      }

      6. Probe with the key before you resend anything

      API call

      POST /api/transactions/get-by-idempotency-key-async?merchantId={{merchantId}}&idempotencyKey={{retryKey}}

      This is the step that stands in for what your service does after a timeout. Ask the lookup about the key you sent. A transaction comes back, so the request completed and must not be sent again, whatever your own client reported. Only a 404 says the merchant has never seen the key, and only that licenses a resend, under the same key so the platform can recognise it. Probe, then decide. Never resend and hope.

      Reference for this operation

      Values this step gives you

        cURL

        curl -X POST -G "{{BASE_URL}}/api/transactions/get-by-idempotency-key-async" \
          --data-urlencode "merchantId={{merchantId}}" \
          --data-urlencode "idempotencyKey={{retryKey}}" \
          -H "api-key: {{API_KEY}}"
        .NET

        // The recovery path, as your service would run it: the send threw, so ask.
        var probe = await http.PostAsync(
            $"/api/transactions/get-by-idempotency-key-async"
            + $"?merchantId={merchantId}&idempotencyKey={Uri.EscapeDataString(retryKey)}",
            content: null);
        
        if (probe.StatusCode == HttpStatusCode.NotFound)
        {
            // Safe to resend, under the same key.
        }
        else
        {
            probe.EnsureSuccessStatusCode();
        
            // It completed. Reconcile against this record and send nothing.
            var completed = await probe.Content.ReadFromJsonAsync<JsonElement>();
            var completedId = completed.GetProperty("id").GetString();
        }

        What this step answers with

        Abridged to the properties this step depends on. A real response carries more.

        HTTP 200

        {
          "id": "5c8d2e1f-3a4b-4d6c-9e0f-1a2b3c4d5e6f",
          "merchantId": "{{merchantId}}",
          "transactionType": "Sale",
          "idempotencyStatus": "Replayed",
          "resultCode": "Ok",
          "authorizedAmount": 10.00,
          "responseData": {
            "resultCode": "Ok",
            "resultMessage": "Approved"
          }
        }

        Keep your keys disciplined

        7. Give one logical request one key, and keep it

        On your side

        Three rules carry the whole practice. One key per logical request, so a key names a payment and not an attempt. Never reuse a key across different payloads, because a key pointed at two different requests is a question with two answers, and you won't like the one you get. Store the key with the order before you send, not after, so a process that died mid-request still knows what to ask about. Follow-up operations take an idempotencyKey too, on the endpoint below: a capture or a refund that runs twice is the same defect as a sale that does, and the same probe recovers it.

        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
          # Retry a payment without a double charge
          #
          # Send a payment under a key you chose, ask the platform what became of it, and retry a request you
          # never got an answer to without charging the cardholder twice.
          #
          # 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}}"
          
          # Values you supply. Set each one before you run the script.
          # The key this payment is sent under. You choose it, so nothing reads it off a response.
          idempotencyKey=""
          # The key the slow payment is sent under. Yours to choose, and different from the first.
          retryKey=""
          
          # 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"
          }
          
          # Percent-encodes a value for use in a URL.
          urlencode() {
            jq -rn --arg value "$1" '$value | @uri'
          }
          
          # Phase 1: Send a payment under a key you chose
          
          # Step 1: Choose a key for this payment
          # Generate a key your own system can reproduce for this one payment and no other. A UUID is fine,
          # and so is an order identifier, as long as one logical request gets one key. Generate it before you
          # send, not after: a key you make up while retrying is a different key, and a different key is a
          # second charge. Use a fresh one each time you work through this page.
          
          # Step 2: Send the sale with the key attached
          body=$(jq -n --arg idempotencyKey "$idempotencyKey" '{
            "transactionType": "Sale",
            "idempotencyKey": $idempotencyKey,
            "cardData": {
              "cardNumber": "4111111111111111",
              "nameOnCard": "Jane Doe",
              "expirationMonth": 12,
              "expirationYear": 2030,
              "cvv": 123
            },
            "invoiceData": {
              "amounts": { "base": 10.00, "total": 10.00 }
            }
          }')
          step2=$(call POST "/api/transactions" "$body")
          transactionId=$(jq -er '.id | if . == null then error("The response carried no value for transactionId.") else tostring end' <<< "$step2")
          merchantId=$(jq -er '.merchantId | if . == null then error("The response carried no value for merchantId.") else tostring end' <<< "$step2")
          
          # Phase 2: Ask what became of that key
          
          # Step 3: Look the key up and see it answer the same transaction
          call POST "/api/transactions/get-by-idempotency-key-async?merchantId=$(urlencode "$merchantId")&idempotencyKey=$(urlencode "$idempotencyKey")" > /dev/null
          
          # Phase 3: Lose the answer, then recover from it
          
          # Step 4: Choose a key for the payment you are about to lose
          # A second key, for a second payment. Reusing the first one here would be the mistake the closing
          # note is about: the key identifies a request, not a caller, and pointing it at a different payload
          # asks the platform a question that has two answers. Keep this one too. The next step is written to
          # make you glad you did.
          
          # Step 5: Send a payment through a degraded processor
          body=$(jq -n --arg retryKey "$retryKey" '{
            "transactionType": "Sale",
            "idempotencyKey": $retryKey,
            "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 }
            }
          }')
          call POST "/api/transactions" "$body" > /dev/null
          
          # Step 6: Probe with the key before you resend anything
          call POST "/api/transactions/get-by-idempotency-key-async?merchantId=$(urlencode "$merchantId")&idempotencyKey=$(urlencode "$retryKey")" > /dev/null
          
          # Phase 4: Keep your keys disciplined
          
          # Step 7: Give one logical request one key, and keep it
          # Three rules carry the whole practice. One key per logical request, so a key names a payment and
          # not an attempt. Never reuse a key across different payloads, because a key pointed at two
          # different requests is a question with two answers, and you won't like the one you get. Store the
          # key with the order before you send, not after, so a process that died mid-request still knows what
          # to ask about. Follow-up operations take an idempotencyKey too, on the endpoint below: a capture or
          # a refund that runs twice is the same defect as a sale that does, and the same probe recovers it.
          

          PowerShell

          # Retry a payment without a double charge
          #
          # Send a payment under a key you chose, ask the platform what became of it, and retry a request you
          # never got an answer to without charging the cardholder twice.
          #
          # 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}}'
          
          # Values you supply. Set each one before you run the script.
          # The key this payment is sent under. You choose it, so nothing reads it off a response.
          $idempotencyKey = ''
          # The key the slow payment is sent under. Yours to choose, and different from the first.
          $retryKey = ''
          
          # 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
          }
          
          # Turns a value read off a response back into the text the API sent. A missing value stops the
          # script.
          function ConvertTo-CaptureValue($Value, [string] $Name) {
              if ($null -eq $Value) { throw "The response carried no value for $Name." }
              if ($Value -is [datetime] -and $Value.Kind -eq 'Unspecified') { return $Value.ToString('yyyy-MM-ddTHH:mm:ss.FFFFFFF', [cultureinfo]::InvariantCulture) }
              if ($Value -is [datetime]) { return $Value.ToUniversalTime().ToString('o') }
              if ($Value -is [bool]) { return $Value.ToString().ToLowerInvariant() }
              [string]$Value
          }
          
          # Phase 1: Send a payment under a key you chose
          
          # Step 1: Choose a key for this payment
          # Generate a key your own system can reproduce for this one payment and no other. A UUID is fine,
          # and so is an order identifier, as long as one logical request gets one key. Generate it before you
          # send, not after: a key you make up while retrying is a different key, and a different key is a
          # second charge. Use a fresh one each time you work through this page.
          
          # Step 2: Send the sale with the key attached
          $body = @"
          {
            "transactionType": "Sale",
            "idempotencyKey": $(ConvertTo-Json -InputObject ([string]($idempotencyKey))),
            "cardData": {
              "cardNumber": "4111111111111111",
              "nameOnCard": "Jane Doe",
              "expirationMonth": 12,
              "expirationYear": 2030,
              "cvv": 123
            },
            "invoiceData": {
              "amounts": { "base": 10.00, "total": 10.00 }
            }
          }
          "@
          $step2 = Invoke-BlueprintCall -Method 'POST' -Path '/api/transactions' -Body $body
          $transactionId = ConvertTo-CaptureValue $step2.id 'transactionId'
          $merchantId = ConvertTo-CaptureValue $step2.merchantId 'merchantId'
          
          # Phase 2: Ask what became of that key
          
          # Step 3: Look the key up and see it answer the same transaction
          $null = Invoke-BlueprintCall -Method 'POST' -Path "/api/transactions/get-by-idempotency-key-async?merchantId=$([uri]::EscapeDataString($merchantId))&idempotencyKey=$([uri]::EscapeDataString($idempotencyKey))"
          
          # Phase 3: Lose the answer, then recover from it
          
          # Step 4: Choose a key for the payment you are about to lose
          # A second key, for a second payment. Reusing the first one here would be the mistake the closing
          # note is about: the key identifies a request, not a caller, and pointing it at a different payload
          # asks the platform a question that has two answers. Keep this one too. The next step is written to
          # make you glad you did.
          
          # Step 5: Send a payment through a degraded processor
          $body = @"
          {
            "transactionType": "Sale",
            "idempotencyKey": $(ConvertTo-Json -InputObject ([string]($retryKey))),
            "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 6: Probe with the key before you resend anything
          $null = Invoke-BlueprintCall -Method 'POST' -Path "/api/transactions/get-by-idempotency-key-async?merchantId=$([uri]::EscapeDataString($merchantId))&idempotencyKey=$([uri]::EscapeDataString($retryKey))"
          
          # Phase 4: Keep your keys disciplined
          
          # Step 7: Give one logical request one key, and keep it
          # Three rules carry the whole practice. One key per logical request, so a key names a payment and
          # not an attempt. Never reuse a key across different payloads, because a key pointed at two
          # different requests is a question with two answers, and you won't like the one you get. Store the
          # key with the order before you send, not after, so a process that died mid-request still knows what
          # to ask about. Follow-up operations take an idempotencyKey too, on the endpoint below: a capture or
          # a refund that runs twice is the same defect as a sale that does, and the same probe recovers it.
          

          TypeScript

          // Retry a payment without a double charge
          //
          // Send a payment under a key you chose, ask the platform what became of it, and retry a request you
          // never got an answer to without charging the cardholder twice.
          //
          // 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}}';
          
          // Values you supply. Set each one before you run the script.
          // The key this payment is sent under. You choose it, so nothing reads it off a response.
          const idempotencyKey = '';
          // The key the slow payment is sent under. Yours to choose, and different from the first.
          const retryKey = '';
          
          // 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;
          }
          
          // Turns a value read off a response into the text the API sent. A missing value throws.
          function capture(value: unknown, name: string): string {
            if (value === undefined || value === null) {
              throw new Error(`The response carried no value for ${name}.`);
            }
          
            return typeof value === 'string' ? value : JSON.stringify(value);
          }
          
          // Phase 1: Send a payment under a key you chose
          
          // Step 1: Choose a key for this payment
          // Generate a key your own system can reproduce for this one payment and no other. A UUID is fine,
          // and so is an order identifier, as long as one logical request gets one key. Generate it before
          // you send, not after: a key you make up while retrying is a different key, and a different key is
          // a second charge. Use a fresh one each time you work through this page.
          
          // Step 2: Send the sale with the key attached
          const step2 = await call('POST', '/api/transactions', {
            "transactionType": "Sale",
            "idempotencyKey": idempotencyKey,
            "cardData": {
              "cardNumber": "4111111111111111",
              "nameOnCard": "Jane Doe",
              "expirationMonth": 12,
              "expirationYear": 2030,
              "cvv": 123
            },
            "invoiceData": {
              "amounts": { "base": 10.00, "total": 10.00 }
            }
          });
          const transactionId = capture(step2.id, 'transactionId');
          const merchantId = capture(step2.merchantId, 'merchantId');
          
          // Phase 2: Ask what became of that key
          
          // Step 3: Look the key up and see it answer the same transaction
          await call('POST', `/api/transactions/get-by-idempotency-key-async?merchantId=${encodeURIComponent(merchantId)}&idempotencyKey=${encodeURIComponent(idempotencyKey)}`);
          
          // Phase 3: Lose the answer, then recover from it
          
          // Step 4: Choose a key for the payment you are about to lose
          // A second key, for a second payment. Reusing the first one here would be the mistake the closing
          // note is about: the key identifies a request, not a caller, and pointing it at a different payload
          // asks the platform a question that has two answers. Keep this one too. The next step is written to
          // make you glad you did.
          
          // Step 5: Send a payment through a degraded processor
          await call('POST', '/api/transactions', {
            "transactionType": "Sale",
            "idempotencyKey": retryKey,
            "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 6: Probe with the key before you resend anything
          await call('POST', `/api/transactions/get-by-idempotency-key-async?merchantId=${encodeURIComponent(merchantId)}&idempotencyKey=${encodeURIComponent(retryKey)}`);
          
          // Phase 4: Keep your keys disciplined
          
          // Step 7: Give one logical request one key, and keep it
          // Three rules carry the whole practice. One key per logical request, so a key names a payment and
          // not an attempt. Never reuse a key across different payloads, because a key pointed at two
          // different requests is a question with two answers, and you won't like the one you get. Store the
          // key with the order before you send, not after, so a process that died mid-request still knows
          // what to ask about. Follow-up operations take an idempotencyKey too, on the endpoint below: a
          // capture or a refund that runs twice is the same defect as a sale that does, and the same probe
          // recovers it.
          

          C#

          // Retry a payment without a double charge
          //
          // Send a payment under a key you chose, ask the platform what became of it, and retry a request you
          // never got an answer to without charging the cardholder twice.
          //
          // 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}}";
          
          // Values you supply. Set each one before you run the script.
          // The key this payment is sent under. You choose it, so nothing reads it off a response.
          var idempotencyKey = "";
          // The key the slow payment is sent under. Yours to choose, and different from the first.
          var retryKey = "";
          
          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);
          }
          
          // Turns a value read off a response into the text the API sent. A missing value throws.
          static string Capture(JsonElement value, string name) => value.ValueKind switch
          {
              JsonValueKind.String => value.GetString()!,
              JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => value.GetRawText(),
              _ => throw new InvalidOperationException($"The response carried no value for {name}.")
          };
          
          // Phase 1: Send a payment under a key you chose
          
          // Step 1: Choose a key for this payment
          // Generate a key your own system can reproduce for this one payment and no other. A UUID is fine,
          // and so is an order identifier, as long as one logical request gets one key. Generate it before
          // you send, not after: a key you make up while retrying is a different key, and a different key is
          // a second charge. Use a fresh one each time you work through this page.
          
          // Step 2: Send the sale with the key attached
          var step2 = await CallAsync("POST", "/api/transactions", $$"""
              {
                "transactionType": "Sale",
                "idempotencyKey": {{JsonSerializer.Serialize(idempotencyKey)}},
                "cardData": {
                  "cardNumber": "4111111111111111",
                  "nameOnCard": "Jane Doe",
                  "expirationMonth": 12,
                  "expirationYear": 2030,
                  "cvv": 123
                },
                "invoiceData": {
                  "amounts": { "base": 10.00, "total": 10.00 }
                }
              }
              """);
          var transactionId = Capture(step2.GetProperty("id"), "transactionId");
          var merchantId = Capture(step2.GetProperty("merchantId"), "merchantId");
          
          // Phase 2: Ask what became of that key
          
          // Step 3: Look the key up and see it answer the same transaction
          await CallAsync("POST", $"/api/transactions/get-by-idempotency-key-async?merchantId={Uri.EscapeDataString(merchantId)}&idempotencyKey={Uri.EscapeDataString(idempotencyKey)}");
          
          // Phase 3: Lose the answer, then recover from it
          
          // Step 4: Choose a key for the payment you are about to lose
          // A second key, for a second payment. Reusing the first one here would be the mistake the closing
          // note is about: the key identifies a request, not a caller, and pointing it at a different payload
          // asks the platform a question that has two answers. Keep this one too. The next step is written to
          // make you glad you did.
          
          // Step 5: Send a payment through a degraded processor
          await CallAsync("POST", "/api/transactions", $$"""
              {
                "transactionType": "Sale",
                "idempotencyKey": {{JsonSerializer.Serialize(retryKey)}},
                "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 6: Probe with the key before you resend anything
          await CallAsync("POST", $"/api/transactions/get-by-idempotency-key-async?merchantId={Uri.EscapeDataString(merchantId)}&idempotencyKey={Uri.EscapeDataString(retryKey)}");
          
          // Phase 4: Keep your keys disciplined
          
          // Step 7: Give one logical request one key, and keep it
          // Three rules carry the whole practice. One key per logical request, so a key names a payment and
          // not an attempt. Never reuse a key across different payloads, because a key pointed at two
          // different requests is a question with two answers, and you won't like the one you get. Store the
          // key with the order before you send, not after, so a process that died mid-request still knows
          // what to ask about. Follow-up operations take an idempotencyKey too, on the endpoint below: a
          // capture or a refund that runs twice is the same defect as a sale that does, and the same probe
          // recovers it.
          

          pip install requests

          Python

          # Retry a payment without a double charge
          #
          # Send a payment under a key you chose, ask the platform what became of it, and retry a request you
          # never got an answer to without charging the cardholder twice.
          #
          # 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.
          
          from urllib.parse import quote
          
          import json
          import requests
          
          BASE_URL = "{{BASE_URL}}"
          API_KEY = "{{API_KEY}}"
          
          # Values you supply. Set each one before you run the script.
          # The key this payment is sent under. You choose it, so nothing reads it off a response.
          idempotency_key = ""
          # The key the slow payment is sent under. Yours to choose, and different from the first.
          retry_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
          
          
          # Turns a value read off a response into the text the API sent. A missing value raises.
          def capture(value, name):
              if value is None:
                  raise ValueError(f"The response carried no value for {name}.")
              return value if isinstance(value, str) else json.dumps(value)
          
          
          # Phase 1: Send a payment under a key you chose
          
          # Step 1: Choose a key for this payment
          # Generate a key your own system can reproduce for this one payment and no other. A UUID is fine,
          # and so is an order identifier, as long as one logical request gets one key. Generate it before you
          # send, not after: a key you make up while retrying is a different key, and a different key is a
          # second charge. Use a fresh one each time you work through this page.
          
          # Step 2: Send the sale with the key attached
          step2 = call("POST", "/api/transactions", {
            "transactionType": "Sale",
            "idempotencyKey": idempotency_key,
            "cardData": {
              "cardNumber": "4111111111111111",
              "nameOnCard": "Jane Doe",
              "expirationMonth": 12,
              "expirationYear": 2030,
              "cvv": 123
            },
            "invoiceData": {
              "amounts": { "base": 10.00, "total": 10.00 }
            }
          })
          transaction_id = capture(step2["id"], "transactionId")
          merchant_id = capture(step2["merchantId"], "merchantId")
          
          # Phase 2: Ask what became of that key
          
          # Step 3: Look the key up and see it answer the same transaction
          call("POST", f"/api/transactions/get-by-idempotency-key-async?merchantId={quote(merchant_id, safe='')}&idempotencyKey={quote(idempotency_key, safe='')}")
          
          # Phase 3: Lose the answer, then recover from it
          
          # Step 4: Choose a key for the payment you are about to lose
          # A second key, for a second payment. Reusing the first one here would be the mistake the closing
          # note is about: the key identifies a request, not a caller, and pointing it at a different payload
          # asks the platform a question that has two answers. Keep this one too. The next step is written to
          # make you glad you did.
          
          # Step 5: Send a payment through a degraded processor
          call("POST", "/api/transactions", {
            "transactionType": "Sale",
            "idempotencyKey": retry_key,
            "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 6: Probe with the key before you resend anything
          call("POST", f"/api/transactions/get-by-idempotency-key-async?merchantId={quote(merchant_id, safe='')}&idempotencyKey={quote(retry_key, safe='')}")
          
          # Phase 4: Keep your keys disciplined
          
          # Step 7: Give one logical request one key, and keep it
          # Three rules carry the whole practice. One key per logical request, so a key names a payment and
          # not an attempt. Never reuse a key across different payloads, because a key pointed at two
          # different requests is a question with two answers, and you won't like the one you get. Store the
          # key with the order before you send, not after, so a process that died mid-request still knows what
          # to ask about. Follow-up operations take an idempotencyKey too, on the endpoint below: a capture or
          # a refund that runs twice is the same defect as a sale that does, and the same probe recovers it.
          

          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.