# Retry a payment without a double charge

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

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

## Send a payment under a key you chose

### 1. Choose a key for this payment

On your side

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

**Values this step gives you**

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

### 2. Send the sale with the key attached

API call

`POST /api/transactions`

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

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

**Values this step gives you**

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

cURL:

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

.NET:

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

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

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

response.EnsureSuccessStatusCode();

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

**What this step answers with** (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",
  "idempotencyStatus": "KeyAccepted",
  "resultCode": "Ok",
  "authorizedAmount": 10.00,
  "responseData": {
    "resultCode": "Ok",
    "resultMessage": "Approved"
  }
}
```

## Ask what became of that key

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

API call

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

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

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

cURL:

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

.NET:

```csharp
var lookup = await http.PostAsync(
    $"/api/transactions/get-by-idempotency-key-async"
    + $"?merchantId={merchantId}&idempotencyKey={Uri.EscapeDataString(idempotencyKey)}",
    content: null);

if (lookup.StatusCode == HttpStatusCode.NotFound)
{
    // Nothing was charged under this key. This is the only answer that licenses
    // a resend.
}
else
{
    lookup.EnsureSuccessStatusCode();

    var existing = await lookup.Content.ReadFromJsonAsync<JsonElement>();
    var existingId = existing.GetProperty("id").GetString();
}
```

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

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

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

## Lose the answer, then recover from it

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

On your side

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

**Values this step gives you**

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

### 5. Send a payment through a degraded processor

API call

`POST /api/transactions`

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

The same sale under the new key, with one extra custom field. Set "loopback.latencyProfile" to the value "slow" and the sandbox processor answers like a degraded one. This run still completes, and it's meant to. What it shows you is the shape of the problem: a request still in flight after you have stopped being sure of it. To lose the answer outright, send "timeout" instead and drive it from your own client with your own timeout set, rather than from this page. One constraint applies to the custom-field channel. If the merchant has defined any custom fields at all, every submitted custom-field name has to match one of those definitions, and a name that doesn't match is rejected with a 400. A sandbox merchant with no custom-field definitions accepts any name, which is the usual case. If you get a 400 naming the control field you sent, define a custom field with that name on the merchant.

[Simulate processor latency](https://devportal-simpay-sbx.winkpg.io/docs/blueprints/simulate-processor-latency.md)

cURL:

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

.NET:

```csharp
var retryKey = "{{retryKey}}";

try
{
    var slow = await http.PostAsJsonAsync("/api/transactions", new
    {
        transactionType = "Sale",
        idempotencyKey = retryKey,
        cardData = new
        {
            cardNumber = "4111111111111111",
            nameOnCard = "Jane Doe",
            expirationMonth = 12,
            expirationYear = 2030,
            cvv = 123
        },
        customFields = new[]
        {
            new { name = "loopback.latencyProfile", value = "slow" }
        },
        invoiceData = new
        {
            amounts = new { @base = 10.00m, total = 10.00m }
        }
    });

    slow.EnsureSuccessStatusCode();
}
catch (TaskCanceledException)
{
    // Your client gave up. The request may still have completed, so the next step
    // is the probe, never a resend.
}
```

### 6. Probe with the key before you resend anything

API call

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

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

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

cURL:

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

.NET:

```csharp
// The recovery path, as your service would run it: the send threw, so ask.
var probe = await http.PostAsync(
    $"/api/transactions/get-by-idempotency-key-async"
    + $"?merchantId={merchantId}&idempotencyKey={Uri.EscapeDataString(retryKey)}",
    content: null);

if (probe.StatusCode == HttpStatusCode.NotFound)
{
    // Safe to resend, under the same key.
}
else
{
    probe.EnsureSuccessStatusCode();

    // It completed. Reconcile against this record and send nothing.
    var completed = await probe.Content.ReadFromJsonAsync<JsonElement>();
    var completedId = completed.GetProperty("id").GetString();
}
```

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

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

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

## Keep your keys disciplined

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

On your side

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

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

## 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
# Retry a payment without a double charge
#
# Send a payment under a key you chose, ask the platform what became of it, and retry a request you
# never got an answer to without charging the cardholder twice.
#
# Every API call in this blueprint, in order. Each value a call returns is passed to the calls after
# it. A step that happens outside the API is a comment, and any failed call stops the script.

set -euo pipefail

BASE_URL="{{BASE_URL}}"
API_KEY="{{API_KEY}}"

# Values you supply. Set each one before you run the script.
# The key this payment is sent under. You choose it, so nothing reads it off a response.
idempotencyKey=""
# The key the slow payment is sent under. Yours to choose, and different from the first.
retryKey=""

# Sends one request and prints the response body. A failed call prints the API's answer and stops
# the script.
call() {
  local method="$1" path="$2" body="${3:-}" out
  local args=(-sS --fail-with-body -X "$method" "$BASE_URL$path" -H "api-key: $API_KEY")
  if [ -n "$body" ]; then
    args+=(-H "Content-Type: application/json" -d "$body")
  fi
  if ! out=$(curl "${args[@]}"); then
    printf '%s\n' "$out" >&2
    return 1
  fi
  printf '%s' "$out"
}

# Percent-encodes a value for use in a URL.
urlencode() {
  jq -rn --arg value "$1" '$value | @uri'
}

# Phase 1: Send a payment under a key you chose

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

# Step 2: Send the sale with the key attached
body=$(jq -n --arg idempotencyKey "$idempotencyKey" '{
  "transactionType": "Sale",
  "idempotencyKey": $idempotencyKey,
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "invoiceData": {
    "amounts": { "base": 10.00, "total": 10.00 }
  }
}')
step2=$(call POST "/api/transactions" "$body")
transactionId=$(jq -er '.id | if . == null then error("The response carried no value for transactionId.") else tostring end' <<< "$step2")
merchantId=$(jq -er '.merchantId | if . == null then error("The response carried no value for merchantId.") else tostring end' <<< "$step2")

# Phase 2: Ask what became of that key

# Step 3: Look the key up and see it answer the same transaction
call POST "/api/transactions/get-by-idempotency-key-async?merchantId=$(urlencode "$merchantId")&idempotencyKey=$(urlencode "$idempotencyKey")" > /dev/null

# Phase 3: Lose the answer, then recover from it

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

# Step 5: Send a payment through a degraded processor
body=$(jq -n --arg retryKey "$retryKey" '{
  "transactionType": "Sale",
  "idempotencyKey": $retryKey,
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "customFields": [
    { "name": "loopback.latencyProfile", "value": "slow" }
  ],
  "invoiceData": {
    "amounts": { "base": 10.00, "total": 10.00 }
  }
}')
call POST "/api/transactions" "$body" > /dev/null

# Step 6: Probe with the key before you resend anything
call POST "/api/transactions/get-by-idempotency-key-async?merchantId=$(urlencode "$merchantId")&idempotencyKey=$(urlencode "$retryKey")" > /dev/null

# Phase 4: Keep your keys disciplined

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

### PowerShell

```powershell
# Retry a payment without a double charge
#
# Send a payment under a key you chose, ask the platform what became of it, and retry a request you
# never got an answer to without charging the cardholder twice.
#
# Every API call in this blueprint, in order. Each value a call returns is passed to the calls after
# it. A step that happens outside the API is a comment, and any failed call stops the script.

$ErrorActionPreference = 'Stop'

$baseUrl = '{{BASE_URL}}'
$apiKey = '{{API_KEY}}'

# Values you supply. Set each one before you run the script.
# The key this payment is sent under. You choose it, so nothing reads it off a response.
$idempotencyKey = ''
# The key the slow payment is sent under. Yours to choose, and different from the first.
$retryKey = ''

# Sends one request and returns the parsed response body. A failed call stops the script.
function Invoke-BlueprintCall([string] $Method, [string] $Path, [string] $Body) {
    $arguments = @{
        Method  = $Method
        Uri     = $baseUrl + $Path
        Headers = @{ 'api-key' = $apiKey }
    }
    if ($Body) {
        $arguments.ContentType = 'application/json'
        $arguments.Body = [System.Text.Encoding]::UTF8.GetBytes($Body)
    }
    Invoke-RestMethod @arguments
}

# Turns a value read off a response back into the text the API sent. A missing value stops the
# script.
function ConvertTo-CaptureValue($Value, [string] $Name) {
    if ($null -eq $Value) { throw "The response carried no value for $Name." }
    if ($Value -is [datetime] -and $Value.Kind -eq 'Unspecified') { return $Value.ToString('yyyy-MM-ddTHH:mm:ss.FFFFFFF', [cultureinfo]::InvariantCulture) }
    if ($Value -is [datetime]) { return $Value.ToUniversalTime().ToString('o') }
    if ($Value -is [bool]) { return $Value.ToString().ToLowerInvariant() }
    [string]$Value
}

# Phase 1: Send a payment under a key you chose

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

# Step 2: Send the sale with the key attached
$body = @"
{
  "transactionType": "Sale",
  "idempotencyKey": $(ConvertTo-Json -InputObject ([string]($idempotencyKey))),
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "invoiceData": {
    "amounts": { "base": 10.00, "total": 10.00 }
  }
}
"@
$step2 = Invoke-BlueprintCall -Method 'POST' -Path '/api/transactions' -Body $body
$transactionId = ConvertTo-CaptureValue $step2.id 'transactionId'
$merchantId = ConvertTo-CaptureValue $step2.merchantId 'merchantId'

# Phase 2: Ask what became of that key

# Step 3: Look the key up and see it answer the same transaction
$null = Invoke-BlueprintCall -Method 'POST' -Path "/api/transactions/get-by-idempotency-key-async?merchantId=$([uri]::EscapeDataString($merchantId))&idempotencyKey=$([uri]::EscapeDataString($idempotencyKey))"

# Phase 3: Lose the answer, then recover from it

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

# Step 5: Send a payment through a degraded processor
$body = @"
{
  "transactionType": "Sale",
  "idempotencyKey": $(ConvertTo-Json -InputObject ([string]($retryKey))),
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "customFields": [
    { "name": "loopback.latencyProfile", "value": "slow" }
  ],
  "invoiceData": {
    "amounts": { "base": 10.00, "total": 10.00 }
  }
}
"@
$null = Invoke-BlueprintCall -Method 'POST' -Path '/api/transactions' -Body $body

# Step 6: Probe with the key before you resend anything
$null = Invoke-BlueprintCall -Method 'POST' -Path "/api/transactions/get-by-idempotency-key-async?merchantId=$([uri]::EscapeDataString($merchantId))&idempotencyKey=$([uri]::EscapeDataString($retryKey))"

# Phase 4: Keep your keys disciplined

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

### TypeScript

```typescript
// Retry a payment without a double charge
//
// Send a payment under a key you chose, ask the platform what became of it, and retry a request you
// never got an answer to without charging the cardholder twice.
//
// Every API call in this blueprint, in order. Each value a call returns is passed to the calls
// after it. A step that happens outside the API is a comment, and any failed call stops the script.

export {};

const baseUrl = '{{BASE_URL}}';
const apiKey = '{{API_KEY}}';

// Values you supply. Set each one before you run the script.
// The key this payment is sent under. You choose it, so nothing reads it off a response.
const idempotencyKey = '';
// The key the slow payment is sent under. Yours to choose, and different from the first.
const retryKey = '';

// Sends one request and returns the parsed response body. A failed call throws.
async function call(method: string, path: string, body?: unknown): Promise<any> {
  const headers: Record<string, string> = { 'api-key': apiKey };
  if (body !== undefined) {
    headers['Content-Type'] = 'application/json';
  }

  const response = await fetch(baseUrl + path, {
    method,
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
  });

  const text = await response.text();
  if (!response.ok) {
    throw new Error(`${method} ${path} answered ${response.status}: ${text}`);
  }

  return text ? JSON.parse(text) : null;
}

// Turns a value read off a response into the text the API sent. A missing value throws.
function capture(value: unknown, name: string): string {
  if (value === undefined || value === null) {
    throw new Error(`The response carried no value for ${name}.`);
  }

  return typeof value === 'string' ? value : JSON.stringify(value);
}

// Phase 1: Send a payment under a key you chose

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

// Step 2: Send the sale with the key attached
const step2 = await call('POST', '/api/transactions', {
  "transactionType": "Sale",
  "idempotencyKey": idempotencyKey,
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "invoiceData": {
    "amounts": { "base": 10.00, "total": 10.00 }
  }
});
const transactionId = capture(step2.id, 'transactionId');
const merchantId = capture(step2.merchantId, 'merchantId');

// Phase 2: Ask what became of that key

// Step 3: Look the key up and see it answer the same transaction
await call('POST', `/api/transactions/get-by-idempotency-key-async?merchantId=${encodeURIComponent(merchantId)}&idempotencyKey=${encodeURIComponent(idempotencyKey)}`);

// Phase 3: Lose the answer, then recover from it

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

// Step 5: Send a payment through a degraded processor
await call('POST', '/api/transactions', {
  "transactionType": "Sale",
  "idempotencyKey": retryKey,
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "customFields": [
    { "name": "loopback.latencyProfile", "value": "slow" }
  ],
  "invoiceData": {
    "amounts": { "base": 10.00, "total": 10.00 }
  }
});

// Step 6: Probe with the key before you resend anything
await call('POST', `/api/transactions/get-by-idempotency-key-async?merchantId=${encodeURIComponent(merchantId)}&idempotencyKey=${encodeURIComponent(retryKey)}`);

// Phase 4: Keep your keys disciplined

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

### C#

```csharp
// Retry a payment without a double charge
//
// Send a payment under a key you chose, ask the platform what became of it, and retry a request you
// never got an answer to without charging the cardholder twice.
//
// Every API call in this blueprint, in order. Each value a call returns is passed to the calls
// after it. A step that happens outside the API is a comment, and any failed call stops the script.

using System.Text;
using System.Text.Json;

var baseUrl = "{{BASE_URL}}";
var apiKey = "{{API_KEY}}";

// Values you supply. Set each one before you run the script.
// The key this payment is sent under. You choose it, so nothing reads it off a response.
var idempotencyKey = "";
// The key the slow payment is sent under. Yours to choose, and different from the first.
var retryKey = "";

using var http = new HttpClient();
http.DefaultRequestHeaders.Add("api-key", apiKey);

// Sends one request and returns the parsed response body. A failed call throws.
async Task<JsonElement> CallAsync(string method, string path, string? body = null)
{
    using var request = new HttpRequestMessage(new HttpMethod(method), baseUrl + path);
    if (body is not null)
    {
        request.Content = new StringContent(body, Encoding.UTF8, "application/json");
    }

    using var response = await http.SendAsync(request);
    var json = await response.Content.ReadAsStringAsync();
    if (!response.IsSuccessStatusCode)
    {
        throw new HttpRequestException($"{method} {path} answered {(int)response.StatusCode}: {json}");
    }

    return json.Length == 0 ? default : JsonSerializer.Deserialize<JsonElement>(json);
}

// Turns a value read off a response into the text the API sent. A missing value throws.
static string Capture(JsonElement value, string name) => value.ValueKind switch
{
    JsonValueKind.String => value.GetString()!,
    JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => value.GetRawText(),
    _ => throw new InvalidOperationException($"The response carried no value for {name}.")
};

// Phase 1: Send a payment under a key you chose

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

// Step 2: Send the sale with the key attached
var step2 = await CallAsync("POST", "/api/transactions", $$"""
    {
      "transactionType": "Sale",
      "idempotencyKey": {{JsonSerializer.Serialize(idempotencyKey)}},
      "cardData": {
        "cardNumber": "4111111111111111",
        "nameOnCard": "Jane Doe",
        "expirationMonth": 12,
        "expirationYear": 2030,
        "cvv": 123
      },
      "invoiceData": {
        "amounts": { "base": 10.00, "total": 10.00 }
      }
    }
    """);
var transactionId = Capture(step2.GetProperty("id"), "transactionId");
var merchantId = Capture(step2.GetProperty("merchantId"), "merchantId");

// Phase 2: Ask what became of that key

// Step 3: Look the key up and see it answer the same transaction
await CallAsync("POST", $"/api/transactions/get-by-idempotency-key-async?merchantId={Uri.EscapeDataString(merchantId)}&idempotencyKey={Uri.EscapeDataString(idempotencyKey)}");

// Phase 3: Lose the answer, then recover from it

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

// Step 5: Send a payment through a degraded processor
await CallAsync("POST", "/api/transactions", $$"""
    {
      "transactionType": "Sale",
      "idempotencyKey": {{JsonSerializer.Serialize(retryKey)}},
      "cardData": {
        "cardNumber": "4111111111111111",
        "nameOnCard": "Jane Doe",
        "expirationMonth": 12,
        "expirationYear": 2030,
        "cvv": 123
      },
      "customFields": [
        { "name": "loopback.latencyProfile", "value": "slow" }
      ],
      "invoiceData": {
        "amounts": { "base": 10.00, "total": 10.00 }
      }
    }
    """);

// Step 6: Probe with the key before you resend anything
await CallAsync("POST", $"/api/transactions/get-by-idempotency-key-async?merchantId={Uri.EscapeDataString(merchantId)}&idempotencyKey={Uri.EscapeDataString(retryKey)}");

// Phase 4: Keep your keys disciplined

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

### Python

```bash
pip install requests
```

```python
# Retry a payment without a double charge
#
# Send a payment under a key you chose, ask the platform what became of it, and retry a request you
# never got an answer to without charging the cardholder twice.
#
# Every API call in this blueprint, in order. Each value a call returns is passed to the calls after
# it. A step that happens outside the API is a comment, and any failed call stops the script.

from urllib.parse import quote

import json
import requests

BASE_URL = "{{BASE_URL}}"
API_KEY = "{{API_KEY}}"

# Values you supply. Set each one before you run the script.
# The key this payment is sent under. You choose it, so nothing reads it off a response.
idempotency_key = ""
# The key the slow payment is sent under. Yours to choose, and different from the first.
retry_key = ""


# Sends one request and returns the parsed response body. A failed call raises.
def call(method, path, body=None):
    response = requests.request(
        method,
        BASE_URL + path,
        headers={"api-key": API_KEY},
        json=body,
    )
    response.raise_for_status()
    return response.json() if response.content else None


# Turns a value read off a response into the text the API sent. A missing value raises.
def capture(value, name):
    if value is None:
        raise ValueError(f"The response carried no value for {name}.")
    return value if isinstance(value, str) else json.dumps(value)


# Phase 1: Send a payment under a key you chose

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

# Step 2: Send the sale with the key attached
step2 = call("POST", "/api/transactions", {
  "transactionType": "Sale",
  "idempotencyKey": idempotency_key,
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "invoiceData": {
    "amounts": { "base": 10.00, "total": 10.00 }
  }
})
transaction_id = capture(step2["id"], "transactionId")
merchant_id = capture(step2["merchantId"], "merchantId")

# Phase 2: Ask what became of that key

# Step 3: Look the key up and see it answer the same transaction
call("POST", f"/api/transactions/get-by-idempotency-key-async?merchantId={quote(merchant_id, safe='')}&idempotencyKey={quote(idempotency_key, safe='')}")

# Phase 3: Lose the answer, then recover from it

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

# Step 5: Send a payment through a degraded processor
call("POST", "/api/transactions", {
  "transactionType": "Sale",
  "idempotencyKey": retry_key,
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "customFields": [
    { "name": "loopback.latencyProfile", "value": "slow" }
  ],
  "invoiceData": {
    "amounts": { "base": 10.00, "total": 10.00 }
  }
})

# Step 6: Probe with the key before you resend anything
call("POST", f"/api/transactions/get-by-idempotency-key-async?merchantId={quote(merchant_id, safe='')}&idempotencyKey={quote(retry_key, safe='')}")

# Phase 4: Keep your keys disciplined

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

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