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

        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.

          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"
              }
            }

            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.