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

8 steps, 7 API calls

**Products:** Payments, Transactions

Samples use {{API_KEY}} for your API key and {{BASE_URL}} for this instance's API address. Anything else in double braces is a value an earlier step gave you.

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

## Cancel a payment before it settles

### 2. Create a sale you are going to cancel

API call

`POST /api/transactions`

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

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.

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

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

.NET:

```csharp
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** (HTTP 200)

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

```json
{
  "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}}`

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

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.

[Error codes](https://devportal-simpay-sbx.winkpg.io/docs/errors.md)

cURL:

```bash
curl "{{BASE_URL}}/api/transactions/{{transactionId}}" \
  -H "api-key: {{API_KEY}}"
```

.NET:

```csharp
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** (HTTP 200)

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

```json
{
  "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`

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

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.

cURL:

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

```csharp
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** (HTTP 200)

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

```json
{
  "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`

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

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.

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

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

.NET:

```csharp
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** (HTTP 200)

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

```json
{
  "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`

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

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.

cURL:

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

.NET:

```csharp
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** (HTTP 200)

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

```json
{
  "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`

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

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.

**Values this step gives you**

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

[Error codes](https://devportal-simpay-sbx.winkpg.io/docs/errors.md)

cURL:

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

```csharp
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** (HTTP 200)

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

```json
{
  "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}}`

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

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.

cURL:

```bash
curl "{{BASE_URL}}/api/transactions/{{refundTransactionId}}" \
  -H "api-key: {{API_KEY}}"
```

.NET:

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

**What this step answers with** (HTTP 200)

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

```json
{
  "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.

### cURL

```bash
brew install jq
```

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

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

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

```csharp
// 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)}");
```

### Python

```bash
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='')}")
```

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

## See also

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