# Accept your first payment

Take a card sale over the API against the sandbox, read the result back, then prove your decline handling with an amount the sandbox always refuses.

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

Create an API key against a sandbox merchant in the application. Every call below sends it as an api-key header. Keep the key out of source control, which is why the samples on this page leave it as a placeholder. A sandbox merchant routes to the loopback processor, so nothing here reaches a card network.

## Take the payment

### 2. Create a card sale

API call

`POST /api/transactions`

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

Send one request to create and run the sale. The amount below is the sandbox's guaranteed approval, so the simulated card never declines it. If the request is refused instead, the response lists each field the merchant's configuration requires, such as the tax amount and purchase order number of Level 2 data.

**Values this step gives you**

- `{{transactionId}}`: The id of the created transaction, from the response body's id property.

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 created = await response.Content.ReadFromJsonAsync<JsonElement>();
var transactionId = created.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": "9f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "merchantId": "3a7b1c9d-2e4f-4a6b-8c8d-9e0f1a2b3c4d",
  "transactionType": "Sale",
  "resultCode": "Ok",
  "authorizedAmount": 10.00,
  "creationTime": "2026-02-04T18:22:41.517Z",
  "responseData": {
    "resultCode": "Ok",
    "resultMessage": "Approved"
  }
}
```

### 3. Read the transaction back

API call

`GET /api/transactions/{{transactionId}}`

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

Read the transaction you just created and check its response data. Build this in from the start. The create response and the stored transaction are the same record. A reconciliation path that trusts only the create response has nowhere to go when a request times out.

cURL:

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

.NET:

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

**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",
  "resultCode": "Ok",
  "authorizedAmount": 10.00,
  "creationTime": "2026-02-04T18:22:41.517Z",
  "responseData": {
    "resultCode": "Ok",
    "resultMessage": "Approved"
  }
}
```

## Prove your decline handling

### 4. Make the sandbox decline you

API call

`POST /api/transactions`

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

Send the same request with an amount of 10.01. The sandbox refuses it and returns "AUTH DECLINED" in the response body. A refusal is a normal response rather than a transport error, so write that handling now. The testing guide lists every amount the sandbox reacts to.

[Testing your integration](https://devportal-simpay-sbx.winkpg.io/docs/testing.md)

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.01, "total": 10.01 }
    }
  }'
```

**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",
  "resultCode": "Decline",
  "authorizedAmount": null,
  "creationTime": "2026-02-04T18:22:41.517Z",
  "responseData": {
    "resultCode": "Decline",
    "resultMessage": "AUTH DECLINED"
  }
}
```

## 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
# Accept your first payment
#
# Take a card sale over the API against the sandbox, read the result back, then prove your decline
# handling with an amount the sandbox always refuses.
#
# 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
# Create an API key against a sandbox merchant in the application. Every call below sends it as an
# api-key header. Keep the key out of source control, which is why the samples on this page leave it
# as a placeholder. A sandbox merchant routes to the loopback processor, so nothing here reaches a
# card network.

# Phase 2: Take the payment

# Step 2: Create a card sale
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")

# Step 3: Read the transaction back
call GET "/api/transactions/$(urlencode "$transactionId")" > /dev/null

# Phase 3: Prove your decline handling

# Step 4: Make the sandbox decline you
call POST "/api/transactions" '{
  "transactionType": "Sale",
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "invoiceData": {
    "amounts": { "base": 10.01, "total": 10.01 }
  }
}' > /dev/null
```

### PowerShell

```powershell
# Accept your first payment
#
# Take a card sale over the API against the sandbox, read the result back, then prove your decline
# handling with an amount the sandbox always refuses.
#
# 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
# Create an API key against a sandbox merchant in the application. Every call below sends it as an
# api-key header. Keep the key out of source control, which is why the samples on this page leave it
# as a placeholder. A sandbox merchant routes to the loopback processor, so nothing here reaches a
# card network.

# Phase 2: Take the payment

# Step 2: Create a card sale
$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'

# Step 3: Read the transaction back
$null = Invoke-BlueprintCall -Method 'GET' -Path "/api/transactions/$([uri]::EscapeDataString($transactionId))"

# Phase 3: Prove your decline handling

# Step 4: Make the sandbox decline you
$body = @'
{
  "transactionType": "Sale",
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "invoiceData": {
    "amounts": { "base": 10.01, "total": 10.01 }
  }
}
'@
$null = Invoke-BlueprintCall -Method 'POST' -Path '/api/transactions' -Body $body
```

### TypeScript

```typescript
// Accept your first payment
//
// Take a card sale over the API against the sandbox, read the result back, then prove your decline
// handling with an amount the sandbox always refuses.
//
// 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
// Create an API key against a sandbox merchant in the application. Every call below sends it as an
// api-key header. Keep the key out of source control, which is why the samples on this page leave
// it as a placeholder. A sandbox merchant routes to the loopback processor, so nothing here reaches
// a card network.

// Phase 2: Take the payment

// Step 2: Create a card sale
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');

// Step 3: Read the transaction back
await call('GET', `/api/transactions/${encodeURIComponent(transactionId)}`);

// Phase 3: Prove your decline handling

// Step 4: Make the sandbox decline you
await call('POST', '/api/transactions', {
  "transactionType": "Sale",
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "invoiceData": {
    "amounts": { "base": 10.01, "total": 10.01 }
  }
});
```

### C#

```csharp
// Accept your first payment
//
// Take a card sale over the API against the sandbox, read the result back, then prove your decline
// handling with an amount the sandbox always refuses.
//
// 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
// Create an API key against a sandbox merchant in the application. Every call below sends it as an
// api-key header. Keep the key out of source control, which is why the samples on this page leave
// it as a placeholder. A sandbox merchant routes to the loopback processor, so nothing here reaches
// a card network.

// Phase 2: Take the payment

// Step 2: Create a card sale
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");

// Step 3: Read the transaction back
await CallAsync("GET", $"/api/transactions/{Uri.EscapeDataString(transactionId)}");

// Phase 3: Prove your decline handling

// Step 4: Make the sandbox decline you
await CallAsync("POST", "/api/transactions", """
    {
      "transactionType": "Sale",
      "cardData": {
        "cardNumber": "4111111111111111",
        "nameOnCard": "Jane Doe",
        "expirationMonth": 12,
        "expirationYear": 2030,
        "cvv": 123
      },
      "invoiceData": {
        "amounts": { "base": 10.01, "total": 10.01 }
      }
    }
    """);
```

### Python

```bash
pip install requests
```

```python
# Accept your first payment
#
# Take a card sale over the API against the sandbox, read the result back, then prove your decline
# handling with an amount the sandbox always refuses.
#
# 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
# Create an API key against a sandbox merchant in the application. Every call below sends it as an
# api-key header. Keep the key out of source control, which is why the samples on this page leave it
# as a placeholder. A sandbox merchant routes to the loopback processor, so nothing here reaches a
# card network.

# Phase 2: Take the payment

# Step 2: Create a card sale
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")

# Step 3: Read the transaction back
call("GET", f"/api/transactions/{quote(transaction_id, safe='')}")

# Phase 3: Prove your decline handling

# Step 4: Make the sandbox decline you
call("POST", "/api/transactions", {
  "transactionType": "Sale",
  "cardData": {
    "cardNumber": "4111111111111111",
    "nameOnCard": "Jane Doe",
    "expirationMonth": 12,
    "expirationYear": 2030,
    "cvv": 123
  },
  "invoiceData": {
    "amounts": { "base": 10.01, "total": 10.01 }
  }
})
```

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