# 

## When to use two-step payments

* **E-commerce** — authorize at checkout, capture when the order ships
* **Hotels / car rentals** — authorize an estimated amount, capture the final charge
* **Subscriptions** — authorize a recurring amount, capture after usage is calculated


For a comparison of one-step vs. two-step flows, see [Sale vs. authorization](/docs/concepts/transactions/sale-vs-auth).

## Payment state lifecycle

![image](https://files.modern-mermaid.live/images/1776276907982-mermaid-diagram-1776276908167.png)

## Prerequisites

* A valid API key — see [Obtain API credentials](/docs/how-tos/setup/obtain-credentials)
* A TSYS integration configured for card processing


## Step 1: Authorize

Send a `card_auth` transaction to place a hold on the customer's card.

curl
```shell
curl -X POST \
  https://api.dev.paradisegateway.net/v1/transactions \
  -H "Content-Type: application/json" \
  -H "APIKEY: YOUR-API-KEY" \
  -d '{
    "type": "card_auth",
    "amount": 4995,
    "card": {
      "pan": "4111111111111111",
      "expiry_month": "12",
      "expiry_year": "2027",
      "cvv": "123",
      "cardholder_name": "Jane Smith",
      "address": { "line1": "123 Main St", "zip": "62704" }
    }
  }'
```

Python
```python
import os, requests

API_KEY = os.environ["PARADISE_API_KEY"]
BASE = os.environ.get("PARADISE_BASE_URL", "https://api.dev.paradisegateway.net")
HEADERS = {"Content-Type": "application/json", "APIKEY": API_KEY}

auth_resp = requests.post(f"{BASE}/v1/transactions", headers=HEADERS, json={
    "type": "card_auth",
    "amount": 4995,
    "card": {
        "pan": "4111111111111111",
        "expiry_month": "12",
        "expiry_year": "2027",
        "cvv": "123",
        "cardholder_name": "Jane Smith",
        "address": {"line1": "123 Main St", "zip": "62704"},
    },
})
auth = auth_resp.json()
txn_id = auth["id"]
print(f"Authorized: {txn_id} — state: {auth['transaction_state']}")
```

JavaScript
```javascript
const API_KEY = process.env.PARADISE_API_KEY;
const BASE = process.env.PARADISE_BASE_URL || "https://api.dev.paradisegateway.net";
const headers = { "Content-Type": "application/json", "APIKEY": API_KEY };

const authResp = await fetch(`${BASE}/v1/transactions`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    type: "card_auth",
    amount: 4995,
    card: {
      pan: "4111111111111111",
      expiry_month: "12",
      expiry_year: "2027",
      cvv: "123",
      cardholder_name: "Jane Smith",
      address: { line1: "123 Main St", zip: "62704" },
    },
  }),
});
const auth = await authResp.json();
console.log(`Authorized: ${auth.id} — state: ${auth.transaction_state}`);
```

C#
```csharp
using var client = new HttpClient();
var baseUrl = Environment.GetEnvironmentVariable("PARADISE_BASE_URL") ?? "https://api.dev.paradisegateway.net";
client.DefaultRequestHeaders.Add("APIKEY", Environment.GetEnvironmentVariable("PARADISE_API_KEY"));

var authResp = await client.PostAsJsonAsync($"{baseUrl}/v1/transactions", new {
    type = "card_auth",
    amount = 4995,
    card = new {
        pan = "4111111111111111",
        expiry_month = "12",
        expiry_year = "2027",
        cvv = "123",
        cardholder_name = "Jane Smith",
        address = new { line1 = "123 Main St", zip = "62704" },
    },
});
var auth = await authResp.Content.ReadAsStringAsync();
Console.WriteLine(auth);
```

Java
```java
HttpClient client = HttpClient.newHttpClient();
String baseUrl = System.getenv().getOrDefault("PARADISE_BASE_URL", "https://api.dev.paradisegateway.net");

HttpRequest authReq = HttpRequest.newBuilder()
    .uri(URI.create(baseUrl + "/v1/transactions"))
    .header("Content-Type", "application/json")
    .header("APIKEY", System.getenv("PARADISE_API_KEY"))
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "type": "card_auth",
          "amount": 4995,
          "card": {
            "pan": "4111111111111111",
            "expiry_month": "12",
            "expiry_year": "2027",
            "cvv": "123",
            "cardholder_name": "Jane Smith",
            "address": { "line1": "123 Main St", "zip": "62704" }
          }
        }"""))
    .build();
HttpResponse<String> authResp = client.send(authReq, HttpResponse.BodyHandlers.ofString());
System.out.println(authResp.body());
```

Go
```go
payload := `{
  "type": "card_auth",
  "amount": 4995,
  "card": {
    "pan": "4111111111111111",
    "expiry_month": "12",
    "expiry_year": "2027",
    "cvv": "123",
    "cardholder_name": "Jane Smith",
    "address": {"line1": "123 Main St", "zip": "62704"}
  }
}`
req, _ := http.NewRequest("POST", base+"/v1/transactions", strings.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("APIKEY", apiKey)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
```

PHP
```php
$payload = json_encode([
    "type" => "card_auth",
    "amount" => 4995,
    "card" => [
        "pan" => "4111111111111111",
        "expiry_month" => "12",
        "expiry_year" => "2027",
        "cvv" => "123",
        "cardholder_name" => "Jane Smith",
        "address" => ["line1" => "123 Main St", "zip" => "62704"],
    ],
]);
$ch = curl_init("{$base}/v1/transactions");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ["Content-Type: application/json", "APIKEY: {$apiKey}"],
    CURLOPT_POSTFIELDS => $payload,
]);
echo curl_exec($ch);
```

Ruby
```ruby
uri = URI("#{base}/v1/transactions")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["APIKEY"] = api_key
req.body = { type: "card_auth", amount: 4995, card: {
  pan: "4111111111111111", expiry_month: "12", expiry_year: "2027",
  cvv: "123", cardholder_name: "Jane Smith",
  address: { line1: "123 Main St", zip: "62704" }
} }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.body
```

### Authorization response

```json
{
  "id": "TRANSACTION-01KEW32V6YNV11T33XGDR7TGWC",
  "type": "card_auth",
  "amount": 4995,
  "transaction_state": "authorized",
  "response_status": "approved",
  "response_code": "00",
  "avs_result_code": "Y",
  "cvv_result": "M",
  "correlation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

Save the `id` — you need it for capture or cancel.

Check fraud signals
Review `avs_result_code` and `cvv_result` before fulfilling. If AVS or CVV fails, consider canceling the authorization. See [Fraud prevention](/docs/concepts/security-compliance/fraud-protection).

## Step 2: Capture

When you are ready to charge (for example, when the order ships), capture the authorized amount.

curl
```shell
curl -X POST \
  https://api.dev.paradisegateway.net/v1/transactions/TRANSACTION-01KEW32V6YNV11T33XGDR7TGWC/capture \
  -H "Content-Type: application/json" \
  -H "APIKEY: YOUR-API-KEY" \
  -d '{ "amount": 4995 }'
```

Python
```python
cap_resp = requests.post(
    f"{BASE}/v1/transactions/{txn_id}/capture",
    headers=HEADERS,
    json={"amount": 4995},
)
cap = cap_resp.json()
print(f"Captured: {cap['transaction_state']}")
```

JavaScript
```javascript
const capResp = await fetch(`${BASE}/v1/transactions/${auth.id}/capture`, {
  method: "POST",
  headers,
  body: JSON.stringify({ amount: 4995 }),
});
const cap = await capResp.json();
console.log(`Captured: ${cap.transaction_state}`);
```

### Partial capture

To capture less than the authorized amount (for example, partial shipment), specify a smaller `amount`. The remaining hold is released.

```shell
curl -X POST \
  https://api.dev.paradisegateway.net/v1/transactions/TRANSACTION-01KEW32V6YNV11T33XGDR7TGWC/capture \
  -H "Content-Type: application/json" \
  -H "APIKEY: YOUR-API-KEY" \
  -d '{ "amount": 2500 }'
```

## Step 3: Handle edge cases

### Cancel before capture

If the order is canceled before capture, void the authorization to release the hold.

```shell
curl -X POST \
  https://api.dev.paradisegateway.net/v1/transactions/TRANSACTION-01KEW32V6YNV11T33XGDR7TGWC/cancel \
  -H "Content-Type: application/json" \
  -H "APIKEY: YOUR-API-KEY"
```

### Authorization expired

Authorization holds typically expire after 7–10 days. If the hold expires before capture, you must create a new authorization.

## Timing rules

```mermaid
gantt
    title Auth/capture timeline
    dateFormat X
    axisFormat %s
    section Authorization
    Hold placed           : 0, 1
    section Window
    Capture window (7-10 days) : 1, 10
    section Capture
    Capture or cancel     : 10, 11
```

| Event | Timing |
|  --- | --- |
| Authorization created | Immediate |
| Hold placed on card | Immediate |
| Capture window | 7–10 days (processor-dependent) |
| Settlement after capture | Next batch close |


## Best practices

* **Capture promptly** — don't let authorization holds expire. Capture as soon as you're ready.
* **Check AVS/CVV** — review fraud signals on the auth response before fulfilling.
* **Use metadata** — attach your order ID to the transaction for reconciliation.
* **Include idempotency keys** — safe to retry if the capture request times out.
* **Log `correlation_id`** — include it in your order records for support requests.


## Further reading

* [Auth/capture for e-commerce](/docs/tutorials/payment-cookbook/auth-capture-ecommerce)
* [Handle partial captures](/docs/how-tos/process-payments/handle-partial-capture)
* [Cancel an authorized transaction](/docs/how-tos/manage-transactions/cancel-auth)
* [Sale vs. authorization](/docs/concepts/transactions/sale-vs-auth)