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

Refund or void a payment

Undo a payment both ways: cancel one before the batch closes, then credit part of another back after it has settled, and read which of the two a transaction will actually accept.

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.

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

Set up your sandbox

1. Get an API key for a sandbox merchant

On your side

Every call below sends an api-key header. Create a key against a sandbox merchant in the application and keep it out of source control: the samples on this page leave it as a placeholder for exactly that reason. Nothing here reaches a card network, and nothing here is reversible from the cardholder's side, which is what makes the sandbox the right place to get the undo path wrong the first time.

Reference for this operation

Values this step gives you

    Cancel a payment before it settles

    2. Create a sale you are going to cancel

    API call

    POST /api/transactions

    The same sale the first blueprint takes, at the sandbox's guaranteed approving amount. Keep both ids that come back: the transaction id names the charge, and the merchant id is a route segment on every follow-up operation.

    Reference for this operation

    Values this step gives you

    • {{transactionId}} The id of the created sale, from the response body's id property.
    • {{merchantId}} The merchant the sale belongs to, from the response body's merchantId property. It's a segment of the follow-up operations route.
    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
        },
        "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}}");
    
    var response = await http.PostAsJsonAsync("/api/transactions", new
    {
        transactionType = "Sale",
        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();

    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",
      "currentStage": "Authorized",
      "resultCode": "Ok",
      "authorizedAmount": 10.00,
      "allowedActions": ["Reversal", "Repeat"],
      "cumulativeRefundedAmount": 0.00,
      "responseData": {
        "resultCode": "Ok",
        "resultMessage": "Approved"
      }
    }

    3. Read which undo the transaction allows

    API call

    GET /api/transactions/{{transactionId}}

    Read the transaction and look at allowedActions before you undo anything. The platform works out which follow-up operations are valid right now from the settlement state and from what the merchant's processor supports, and you can't derive that answer from your own side. Branch on this list rather than assuming. A flow that always sends one operation type works in your sandbox and fails against the first merchant whose processor answers differently.

    Reference for this operation

    Values this step gives you

      cURL

      curl "{{BASE_URL}}/api/transactions/{{transactionId}}" \
        -H "api-key: {{API_KEY}}"
      .NET

      var sale = await http.GetFromJsonAsync<JsonElement>(
          $"/api/transactions/{transactionId}");
      
      var allowed = sale.GetProperty("allowedActions")
          .EnumerateArray()
          .Select(a => a.GetString())
          .ToArray();
      
      // Before the batch closes this is the cancel pair; after it closes it is Refund.
      var undo = allowed.Contains("Reversal") ? "Reversal"
          : allowed.Contains("Void") ? "Void"
          : "Refund";

      What this step answers with

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

      HTTP 200

      {
        "id": "{{transactionId}}",
        "merchantId": "3a7b1c9d-2e4f-4a6b-8c8d-9e0f1a2b3c4d",
        "transactionType": "Sale",
        "currentStage": "Authorized",
        "resultCode": "Ok",
        "authorizedAmount": 10.00,
        "allowedActions": ["Reversal", "Repeat"],
        "cumulativeRefundedAmount": 0.00,
        "responseData": {
          "resultCode": "Ok",
          "resultMessage": "Approved"
        }
      }

      4. Cancel it before the batch closes

      API call

      POST /api/transactions/by-merchant/{{merchantId}}/{{transactionId}}/operations

      Cancel the charge rather than crediting it, which is what the platform allows up to the batch close. Two operations do that, and the difference is whether the processor hears about it. A Reversal sends an online message that releases the issuer's hold and pulls the charge from the next clearing, and the processor can decline it. A Void is a ledger-only cancel that keeps the charge out of the next batch, and it's what the platform offers when the processor supports no online undo. The sandbox processor supports the online undo, so allowedActions offered Reversal above and that's what this step sends. The cardholder sees no charge either way, which is the whole reason to prefer this over a refund while you still can.

      Reference for this operation

      Values this step gives you

        cURL

        curl -X POST \
          "{{BASE_URL}}/api/transactions/by-merchant/{{merchantId}}/{{transactionId}}/operations" \
          -H "api-key: {{API_KEY}}" \
          -H "Content-Type: application/json" \
          -d '{
            "operationType": "Reversal",
            "reason": "Customer cancelled before shipping"
          }'
        .NET

        var cancel = await http.PostAsJsonAsync(
            $"/api/transactions/by-merchant/{merchantId}/{transactionId}/operations",
            new
            {
                operationType = "Reversal",
                reason = "Customer cancelled before shipping"
            });
        
        cancel.EnsureSuccessStatusCode();
        
        var outcome = await cancel.Content.ReadFromJsonAsync<JsonElement>();
        var succeeded = outcome.GetProperty("success").GetBoolean();

        What this step answers with

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

        HTTP 200

        {
          "success": true,
          "operationType": "Reversal",
          "transactionId": "{{transactionId}}",
          "timedOut": false
        }

        Refund a payment after it settles

        5. Create a second sale to refund later

        API call

        POST /api/transactions

        The first sale is cancelled and terminal, so the refund half needs its own charge. This is the same request again; only the id you keep is different. It belongs to the same merchant, so the merchant id captured above is still the one the operations route takes.

        Reference for this operation

        Values this step gives you

        • {{settledTransactionId}} The id of the second sale, from the response body's id property. This is the charge the refund is issued against once it has settled.
        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
            },
            "invoiceData": {
              "amounts": { "base": 10.00, "total": 10.00 }
            }
          }'
        .NET

        var second = await http.PostAsJsonAsync("/api/transactions", new
        {
            transactionType = "Sale",
            cardData = new
            {
                cardNumber = "4111111111111111",
                nameOnCard = "Jane Doe",
                expirationMonth = 12,
                expirationYear = 2030,
                cvv = 123
            },
            invoiceData = new
            {
                amounts = new { @base = 10.00m, total = 10.00m }
            }
        });
        
        second.EnsureSuccessStatusCode();
        
        var settledTransactionId =
            (await second.Content.ReadFromJsonAsync<JsonElement>())
            .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": "3a7b1c9d-2e4f-4a6b-8c8d-9e0f1a2b3c4d",
          "transactionType": "Sale",
          "currentStage": "Authorized",
          "resultCode": "Ok",
          "authorizedAmount": 10.00,
          "allowedActions": ["Reversal", "Repeat"],
          "cumulativeRefundedAmount": 0.00,
          "responseData": {
            "resultCode": "Ok",
            "resultMessage": "Approved"
          }
        }

        6. Close your sandbox batch

        API call

        POST /api/transactions/settlements/sandbox/close

        Close the batch so the charge settles, because a refund is only valid once it has. In production the close is scheduled, so a real integration reads allowedActions and waits for Refund to appear. In the sandbox you can close the batch yourself, and this call does it synchronously. It returns once the batch has cleared, so the next step can run immediately. It settles everything the merchant has outstanding, not only the sale above, and it's refused for a live key, so nothing you learn here changes when a production batch closes. Read failedBatchCount off the response before you rely on it. A merchant with more than one processor gets one batch per processor, and a non-zero count means one of them didn't settle, so a refund against a sale in that batch is still refused. Closing the batch isn't the only condition: a refund also needs the merchant's Allow Refunds setting on, and closing another batch won't turn it on.

        Reference for this operation

        Values this step gives you

          cURL

          curl -X POST "{{BASE_URL}}/api/transactions/settlements/sandbox/close" \
            -H "api-key: {{API_KEY}}"
          .NET

          var close = await http.PostAsync(
              "/api/transactions/settlements/sandbox/close", content: null);
          
          close.EnsureSuccessStatusCode();
          
          var closed = await close.Content.ReadFromJsonAsync<JsonElement>();
          var settledCount = closed.GetProperty("settledTransactionCount").GetInt32();
          
          // One batch per processor. A non-zero count means one of them did not settle,
          // so the sales it carried are still unsettled and still cannot be refunded.
          if (closed.GetProperty("failedBatchCount").GetInt32() > 0)
          {
              throw new InvalidOperationException(
                  "Part of the batch did not settle. Read the transaction back and check "
                  + "allowedActions before refunding.");
          }

          What this step answers with

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

          HTTP 200

          {
            "merchantId": "3a7b1c9d-2e4f-4a6b-8c8d-9e0f1a2b3c4d",
            "settledTransactionCount": 1,
            "settledTotalAmount": 10.00,
            "failedBatchCount": 0
          }

          7. Refund part of it once it has settled

          API call

          POST /api/transactions/by-merchant/{{merchantId}}/{{settledTransactionId}}/operations

          Send a Refund, which is what the undo becomes after the batch closes. The money has moved by then, so cancelling it no longer means anything. A refund is a credit sent back to the cardholder, not a state change on the original charge. The platform spawns a new Return transaction linked back to the parent, so what you get back is a second transaction id in newTransactionId, and the parent keeps a running cumulativeRefundedAmount. Sending an amount of 5.00 against a sale of 10.00 refunds part of it and leaves the rest refundable later. Against a sale that hasn't settled, this same call is refused with OperationNotAllowedInState, which is what the step before this one exists to prevent. Read allowedActions again if you want to see Refund appear where Reversal used to be. Refund also depends on the merchant's Allow Refunds setting. A sandbox merchant starts with it on, but if a settled sale's allowedActions comes back empty, that setting is off, and the refund is refused with OperationNotAllowedInState until it's turned on.

          Reference for this operation

          Values this step gives you

          • {{refundTransactionId}} The newTransactionId from the refund result: the credit is its own transaction, not a flag on the sale.
          cURL

          curl -X POST \
            "{{BASE_URL}}/api/transactions/by-merchant/{{merchantId}}/{{settledTransactionId}}/operations" \
            -H "api-key: {{API_KEY}}" \
            -H "Content-Type: application/json" \
            -d '{
              "operationType": "Refund",
              "amount": 5.00,
              "reason": "Returned one item"
            }'
          .NET

          var refund = await http.PostAsJsonAsync(
              $"/api/transactions/by-merchant/{merchantId}/{settledTransactionId}/operations",
              new
              {
                  operationType = "Refund",
                  amount = 5.00m,
                  reason = "Returned one item"
              });
          
          refund.EnsureSuccessStatusCode();
          
          var result = await refund.Content.ReadFromJsonAsync<JsonElement>();
          var refundTransactionId = result.GetProperty("newTransactionId").GetString();

          What this step answers with

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

          HTTP 200

          {
            "success": true,
            "operationType": "Refund",
            "transactionId": "{{settledTransactionId}}",
            "newTransactionId": "5c8d2e1f-3a4b-4d6c-9e0f-1a2b3c4d5e6f",
            "timedOut": false
          }

          8. Read the credit back

          API call

          GET /api/transactions/{{refundTransactionId}}

          Read the refund by its own id and you get a Return transaction that ran through the same authorization and settlement pipeline the sale did. That's the point worth taking away. A refund can be declined and it settles on its own schedule, so treating it as done the moment the operation call returns is the reconciliation bug this step exists to prevent.

          Reference for this operation

          Values this step gives you

            cURL

            curl "{{BASE_URL}}/api/transactions/{{refundTransactionId}}" \
              -H "api-key: {{API_KEY}}"
            .NET

            var credit = await http.GetFromJsonAsync<JsonElement>(
                $"/api/transactions/{refundTransactionId}");

            What this step answers with

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

            HTTP 200

            {
              "id": "{{refundTransactionId}}",
              "merchantId": "{{merchantId}}",
              "transactionType": "Return",
              "currentStage": "Authorized",
              "resultCode": "Ok",
              "authorizedAmount": 5.00,
              "primaryChargeTransactionId": "{{settledTransactionId}}",
              "responseData": {
                "resultCode": "Ok",
                "resultMessage": "Approved"
              }
            }

            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
            # Refund or void a payment
            #
            # Undo a payment both ways: cancel one before the batch closes, then credit part of another back
            # after it has settled, and read which of the two a transaction will actually accept.
            #
            # 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"
            }
            
            # Percent-encodes a value for use in a URL.
            urlencode() {
              jq -rn --arg value "$1" '$value | @uri'
            }
            
            # Phase 1: Set up your sandbox
            
            # Step 1: Get an API key for a sandbox merchant
            # Every call below sends an api-key header. Create a key against a sandbox merchant in the
            # application and keep it out of source control: the samples on this page leave it as a placeholder
            # for exactly that reason. Nothing here reaches a card network, and nothing here is reversible from
            # the cardholder's side, which is what makes the sandbox the right place to get the undo path wrong
            # the first time.
            
            # Phase 2: Cancel a payment before it settles
            
            # Step 2: Create a sale you are going to cancel
            step2=$(call POST "/api/transactions" '{
              "transactionType": "Sale",
              "cardData": {
                "cardNumber": "4111111111111111",
                "nameOnCard": "Jane Doe",
                "expirationMonth": 12,
                "expirationYear": 2030,
                "cvv": 123
              },
              "invoiceData": {
                "amounts": { "base": 10.00, "total": 10.00 }
              }
            }')
            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")
            
            # Step 3: Read which undo the transaction allows
            call GET "/api/transactions/$(urlencode "$transactionId")" > /dev/null
            
            # Step 4: Cancel it before the batch closes
            call POST "/api/transactions/by-merchant/$(urlencode "$merchantId")/$(urlencode "$transactionId")/operations" '{
              "operationType": "Reversal",
              "reason": "Customer cancelled before shipping"
            }' > /dev/null
            
            # Phase 3: Refund a payment after it settles
            
            # Step 5: Create a second sale to refund later
            step5=$(call POST "/api/transactions" '{
              "transactionType": "Sale",
              "cardData": {
                "cardNumber": "4111111111111111",
                "nameOnCard": "Jane Doe",
                "expirationMonth": 12,
                "expirationYear": 2030,
                "cvv": 123
              },
              "invoiceData": {
                "amounts": { "base": 10.00, "total": 10.00 }
              }
            }')
            settledTransactionId=$(jq -er '.id | if . == null then error("The response carried no value for settledTransactionId.") else tostring end' <<< "$step5")
            
            # Step 6: Close your sandbox batch
            call POST "/api/transactions/settlements/sandbox/close" > /dev/null
            
            # Step 7: Refund part of it once it has settled
            step7=$(call POST "/api/transactions/by-merchant/$(urlencode "$merchantId")/$(urlencode "$settledTransactionId")/operations" '{
              "operationType": "Refund",
              "amount": 5.00,
              "reason": "Returned one item"
            }')
            refundTransactionId=$(jq -er '.newTransactionId | if . == null then error("The response carried no value for refundTransactionId.") else tostring end' <<< "$step7")
            
            # Step 8: Read the credit back
            call GET "/api/transactions/$(urlencode "$refundTransactionId")" > /dev/null
            

            PowerShell

            # Refund or void a payment
            #
            # Undo a payment both ways: cancel one before the batch closes, then credit part of another back
            # after it has settled, and read which of the two a transaction will actually accept.
            #
            # 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
            }
            
            # 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: Set up your sandbox
            
            # Step 1: Get an API key for a sandbox merchant
            # Every call below sends an api-key header. Create a key against a sandbox merchant in the
            # application and keep it out of source control: the samples on this page leave it as a placeholder
            # for exactly that reason. Nothing here reaches a card network, and nothing here is reversible from
            # the cardholder's side, which is what makes the sandbox the right place to get the undo path wrong
            # the first time.
            
            # Phase 2: Cancel a payment before it settles
            
            # Step 2: Create a sale you are going to cancel
            $body = @'
            {
              "transactionType": "Sale",
              "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'
            
            # Step 3: Read which undo the transaction allows
            $null = Invoke-BlueprintCall -Method 'GET' -Path "/api/transactions/$([uri]::EscapeDataString($transactionId))"
            
            # Step 4: Cancel it before the batch closes
            $body = @'
            {
              "operationType": "Reversal",
              "reason": "Customer cancelled before shipping"
            }
            '@
            $null = Invoke-BlueprintCall -Method 'POST' -Path "/api/transactions/by-merchant/$([uri]::EscapeDataString($merchantId))/$([uri]::EscapeDataString($transactionId))/operations" -Body $body
            
            # Phase 3: Refund a payment after it settles
            
            # Step 5: Create a second sale to refund later
            $body = @'
            {
              "transactionType": "Sale",
              "cardData": {
                "cardNumber": "4111111111111111",
                "nameOnCard": "Jane Doe",
                "expirationMonth": 12,
                "expirationYear": 2030,
                "cvv": 123
              },
              "invoiceData": {
                "amounts": { "base": 10.00, "total": 10.00 }
              }
            }
            '@
            $step5 = Invoke-BlueprintCall -Method 'POST' -Path '/api/transactions' -Body $body
            $settledTransactionId = ConvertTo-CaptureValue $step5.id 'settledTransactionId'
            
            # Step 6: Close your sandbox batch
            $null = Invoke-BlueprintCall -Method 'POST' -Path '/api/transactions/settlements/sandbox/close'
            
            # Step 7: Refund part of it once it has settled
            $body = @'
            {
              "operationType": "Refund",
              "amount": 5.00,
              "reason": "Returned one item"
            }
            '@
            $step7 = Invoke-BlueprintCall -Method 'POST' -Path "/api/transactions/by-merchant/$([uri]::EscapeDataString($merchantId))/$([uri]::EscapeDataString($settledTransactionId))/operations" -Body $body
            $refundTransactionId = ConvertTo-CaptureValue $step7.newTransactionId 'refundTransactionId'
            
            # Step 8: Read the credit back
            $null = Invoke-BlueprintCall -Method 'GET' -Path "/api/transactions/$([uri]::EscapeDataString($refundTransactionId))"
            

            TypeScript

            // Refund or void a payment
            //
            // Undo a payment both ways: cancel one before the batch closes, then credit part of another back
            // after it has settled, and read which of the two a transaction will actually accept.
            //
            // 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;
            }
            
            // 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: Set up your sandbox
            
            // Step 1: Get an API key for a sandbox merchant
            // Every call below sends an api-key header. Create a key against a sandbox merchant in the
            // application and keep it out of source control: the samples on this page leave it as a placeholder
            // for exactly that reason. Nothing here reaches a card network, and nothing here is reversible from
            // the cardholder's side, which is what makes the sandbox the right place to get the undo path wrong
            // the first time.
            
            // Phase 2: Cancel a payment before it settles
            
            // Step 2: Create a sale you are going to cancel
            const step2 = await call('POST', '/api/transactions', {
              "transactionType": "Sale",
              "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');
            
            // Step 3: Read which undo the transaction allows
            await call('GET', `/api/transactions/${encodeURIComponent(transactionId)}`);
            
            // Step 4: Cancel it before the batch closes
            await call('POST', `/api/transactions/by-merchant/${encodeURIComponent(merchantId)}/${encodeURIComponent(transactionId)}/operations`, {
              "operationType": "Reversal",
              "reason": "Customer cancelled before shipping"
            });
            
            // Phase 3: Refund a payment after it settles
            
            // Step 5: Create a second sale to refund later
            const step5 = await call('POST', '/api/transactions', {
              "transactionType": "Sale",
              "cardData": {
                "cardNumber": "4111111111111111",
                "nameOnCard": "Jane Doe",
                "expirationMonth": 12,
                "expirationYear": 2030,
                "cvv": 123
              },
              "invoiceData": {
                "amounts": { "base": 10.00, "total": 10.00 }
              }
            });
            const settledTransactionId = capture(step5.id, 'settledTransactionId');
            
            // Step 6: Close your sandbox batch
            await call('POST', '/api/transactions/settlements/sandbox/close');
            
            // Step 7: Refund part of it once it has settled
            const step7 = await call('POST', `/api/transactions/by-merchant/${encodeURIComponent(merchantId)}/${encodeURIComponent(settledTransactionId)}/operations`, {
              "operationType": "Refund",
              "amount": 5.00,
              "reason": "Returned one item"
            });
            const refundTransactionId = capture(step7.newTransactionId, 'refundTransactionId');
            
            // Step 8: Read the credit back
            await call('GET', `/api/transactions/${encodeURIComponent(refundTransactionId)}`);
            

            C#

            // Refund or void a payment
            //
            // Undo a payment both ways: cancel one before the batch closes, then credit part of another back
            // after it has settled, and read which of the two a transaction will actually accept.
            //
            // 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);
            }
            
            // 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: Set up your sandbox
            
            // Step 1: Get an API key for a sandbox merchant
            // Every call below sends an api-key header. Create a key against a sandbox merchant in the
            // application and keep it out of source control: the samples on this page leave it as a placeholder
            // for exactly that reason. Nothing here reaches a card network, and nothing here is reversible from
            // the cardholder's side, which is what makes the sandbox the right place to get the undo path wrong
            // the first time.
            
            // Phase 2: Cancel a payment before it settles
            
            // Step 2: Create a sale you are going to cancel
            var step2 = await CallAsync("POST", "/api/transactions", """
                {
                  "transactionType": "Sale",
                  "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");
            
            // Step 3: Read which undo the transaction allows
            await CallAsync("GET", $"/api/transactions/{Uri.EscapeDataString(transactionId)}");
            
            // Step 4: Cancel it before the batch closes
            await CallAsync("POST", $"/api/transactions/by-merchant/{Uri.EscapeDataString(merchantId)}/{Uri.EscapeDataString(transactionId)}/operations", """
                {
                  "operationType": "Reversal",
                  "reason": "Customer cancelled before shipping"
                }
                """);
            
            // Phase 3: Refund a payment after it settles
            
            // Step 5: Create a second sale to refund later
            var step5 = await CallAsync("POST", "/api/transactions", """
                {
                  "transactionType": "Sale",
                  "cardData": {
                    "cardNumber": "4111111111111111",
                    "nameOnCard": "Jane Doe",
                    "expirationMonth": 12,
                    "expirationYear": 2030,
                    "cvv": 123
                  },
                  "invoiceData": {
                    "amounts": { "base": 10.00, "total": 10.00 }
                  }
                }
                """);
            var settledTransactionId = Capture(step5.GetProperty("id"), "settledTransactionId");
            
            // Step 6: Close your sandbox batch
            await CallAsync("POST", "/api/transactions/settlements/sandbox/close");
            
            // Step 7: Refund part of it once it has settled
            var step7 = await CallAsync("POST", $"/api/transactions/by-merchant/{Uri.EscapeDataString(merchantId)}/{Uri.EscapeDataString(settledTransactionId)}/operations", """
                {
                  "operationType": "Refund",
                  "amount": 5.00,
                  "reason": "Returned one item"
                }
                """);
            var refundTransactionId = Capture(step7.GetProperty("newTransactionId"), "refundTransactionId");
            
            // Step 8: Read the credit back
            await CallAsync("GET", $"/api/transactions/{Uri.EscapeDataString(refundTransactionId)}");
            

            pip install requests

            Python

            # Refund or void a payment
            #
            # Undo a payment both ways: cancel one before the batch closes, then credit part of another back
            # after it has settled, and read which of the two a transaction will actually accept.
            #
            # 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}}"
            
            
            # 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: Set up your sandbox
            
            # Step 1: Get an API key for a sandbox merchant
            # Every call below sends an api-key header. Create a key against a sandbox merchant in the
            # application and keep it out of source control: the samples on this page leave it as a placeholder
            # for exactly that reason. Nothing here reaches a card network, and nothing here is reversible from
            # the cardholder's side, which is what makes the sandbox the right place to get the undo path wrong
            # the first time.
            
            # Phase 2: Cancel a payment before it settles
            
            # Step 2: Create a sale you are going to cancel
            step2 = call("POST", "/api/transactions", {
              "transactionType": "Sale",
              "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")
            
            # Step 3: Read which undo the transaction allows
            call("GET", f"/api/transactions/{quote(transaction_id, safe='')}")
            
            # Step 4: Cancel it before the batch closes
            call("POST", f"/api/transactions/by-merchant/{quote(merchant_id, safe='')}/{quote(transaction_id, safe='')}/operations", {
              "operationType": "Reversal",
              "reason": "Customer cancelled before shipping"
            })
            
            # Phase 3: Refund a payment after it settles
            
            # Step 5: Create a second sale to refund later
            step5 = call("POST", "/api/transactions", {
              "transactionType": "Sale",
              "cardData": {
                "cardNumber": "4111111111111111",
                "nameOnCard": "Jane Doe",
                "expirationMonth": 12,
                "expirationYear": 2030,
                "cvv": 123
              },
              "invoiceData": {
                "amounts": { "base": 10.00, "total": 10.00 }
              }
            })
            settled_transaction_id = capture(step5["id"], "settledTransactionId")
            
            # Step 6: Close your sandbox batch
            call("POST", "/api/transactions/settlements/sandbox/close")
            
            # Step 7: Refund part of it once it has settled
            step7 = call("POST", f"/api/transactions/by-merchant/{quote(merchant_id, safe='')}/{quote(settled_transaction_id, safe='')}/operations", {
              "operationType": "Refund",
              "amount": 5.00,
              "reason": "Returned one item"
            })
            refund_transaction_id = capture(step7["newTransactionId"], "refundTransactionId")
            
            # Step 8: Read the credit back
            call("GET", f"/api/transactions/{quote(refund_transaction_id, safe='')}")
            

            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.