---
title: Preauthorize and capture
slug: preauthorize-and-capture
docTags: 
createdAt: 2026-02-20T18:38:37.977Z
---

:::hint{type="warning"}
To enable card-on-file features such as this one, including partial captures, you must contact [Jetpay support ](mailto\:jetpay@baselinepayments.com)directly.
:::

This flow lets you authorize a saved card without capturing funds right away, then capture the amount later in one go or across several partial captures.

It suits platforms that need to reserve funds and settle them later. For example, rentals, deposits, usage-based billing, split shipments, or marketplaces confirming fulfillment.

If you want to charge a saved card immediately, see [Directly charge a credit card](docId\:fQy5afJMMgJ3h5ioYttFd) instead.

***

## Prerequisites

- [Getting your API Key](docId:_zgmIm7QKrrAoX9JjaQrL)
- Your [Getting your bank account ID](docId\:aEx0SSFndR_XgalXG2_ZF) where funds will be deposited
- [Tokenize a contact credit card](docId:_5zW0FS71q4rn5rY5IFPj)

Before you preauthorize a saved card, the customer must exist as a Contact in Jetpay and have at least one tokenized payment method attached.

If you have not yet created a contact or collected card details, complete the [Create a contact](docId\:BzwF0wyOoqBKGSFuGEnH0) and [Tokenize a contact credit card](docId:_5zW0FS71q4rn5rY5IFPj) guides first.

***

## Flow overview

```mermaid
sequenceDiagram
    autonumber
    participant Platform
    participant Jetpay
    participant CardNetwork as Card Network

    Platform->>Jetpay: POST /contact/{contact_id}/creditCards/{credit_card_id}/preauthorize
    Jetpay->>CardNetwork: Authorize only
    CardNetwork-->>Jetpay: Authorization result
    Jetpay-->>Platform: Debit created (state: preauthorized)

    loop One or more captures
        Platform->>Jetpay: POST /debit/{debit_id}/captures
        Jetpay->>CardNetwork: Capture request
        Jetpay-->>Platform: Capture created (201)
        Jetpay-->>Platform: Webhook (payments.captures.created)
    end

    opt Remaining balance not needed
        Platform->>Jetpay: POST /debit/{debit_id}/release (or final_capture: true)
        Jetpay->>CardNetwork: Release remaining authorization
        Jetpay-->>Platform: Webhook (payments.preauthorizations.released)
    end
```

***

## Implementation details

### Create a preauthorization

Endpoint: [`POST /contact/{contact_id}/creditCards/{credit_card_id}/preauthorize`](https://extapi.jetpay.baselinepayments.com/docs#tag/Contacts-\(v0\)/operation/preauthorize_contact_credit_card_contact__contact_id__creditCards__credit_card_id__preauthorize_post)

:::CodeblockTabs
```curl
curl https://extapi.demo.jetpay.baselinepayments.com/contact/contact_123/creditCards/cc_456/preauthorize \
  -X POST \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 100.00,
    "to_bank_account_id": "<your_bank_account_id>",
    "statement": "Test Statement"
  }'
```

```python
import requests

url = "https://extapi.demo.jetpay.baselinepayments.com/contact/contact_123/creditCards/cc_456/preauthorize"

headers = {
    "Authorization": "Bearer <YOUR_API_TOKEN>",
    "Content-Type": "application/json"
}

payload = {
    "amount": 100.00,
    "to_bank_account_id": "<your_bank_account_id>",
    "statement": "Test Statement"
}

response = requests.post(url, json=payload, headers=headers)

print(response.status_code)
print(response.json())
```

```nodejs
const fetch = require("node-fetch");

const url = "https://extapi.demo.jetpay.baselinepayments.com/contact/contact_123/creditCards/cc_456/preauthorize";

const payload = {
  amount: 100.00,
  to_bank_account_id: "<your_bank_account_id>",
  statement: "Test Statement"
};

fetch(url, {
  method: "POST",
  headers: {
    "Authorization": "Bearer <YOUR_API_TOKEN>",
    "Content-Type": "application/json"
  },
  body: JSON.stringify(payload)
})
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));
```
:::

### Handle the response

If the card is successfully preauthorized, the endpoint returns HTTP `200`:

```json
{
  "debit_id": "debit_123",
  "amount": 100.00,
  "statement": "Test Statement",
  "to_bank_account_id": "your_bank_account_id",
  "contact_id": "contact_123",
  "from_credit_card_id": "cc_456",
  "transaction_state": "preauthorized"
}
```

Store the returned `debit_id` for the steps that follow.

### Capture part of the amount

Endpoint: [`POST /debit/{debit_id}/captures`](https://extapi.jetpay.baselinepayments.com/docs#tag/Transactions-\(v0\)/operation/capture_debit_debit__transaction_id__captures_post)

Capture as much of the preauthorized amount as you need. You can capture again later, up to the full amount.

Include an `Idempotency-Key` header whenever the body has `capture_amount` or `final_capture`. Without it, the request is refused with a `400`.

:::CodeblockTabs
```curl
curl https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/captures \
  -X POST \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-789-shipment-1" \
  -d '{
    "capture_amount": 40.00
  }'
```

```python
import requests

url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/captures"

headers = {
    "Authorization": "Bearer <YOUR_API_TOKEN>",
    "Content-Type": "application/json",
    "Idempotency-Key": "order-789-shipment-1"
}

payload = {
    "capture_amount": 40.00
}

response = requests.post(url, json=payload, headers=headers)

print(response.status_code)
print(response.json())
```

```nodejs
const fetch = require("node-fetch");

const url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/captures";

const payload = {
  capture_amount: 40.00
};

fetch(url, {
  method: "POST",
  headers: {
    "Authorization": "Bearer <YOUR_API_TOKEN>",
    "Content-Type": "application/json",
    "Idempotency-Key": "order-789-shipment-1"
  },
  body: JSON.stringify(payload)
})
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));
```
:::

### Handle the response

```json
{
  "identifier": "debit_123",
  "state": "preauthorized",
  "capture_id": "capture_001",
  "capture_status": "paid",
  "capture_amount": 40.00,
  "principal_captured": 40.00,
  "captured_amount": 41.20,
  "available_amount": 60.00,
  "final_capture": false,
  "release": null
}
```

### Make the final capture

When you're done capturing, mark the last capture as final. Any remaining balance is released for you.

:::CodeblockTabs
```curl
curl https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/captures \
  -X POST \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-789-shipment-2" \
  -d '{
    "capture_amount": 35.00,
    "final_capture": true
  }'
```

```python
import requests

url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/captures"

headers = {
    "Authorization": "Bearer <YOUR_API_TOKEN>",
    "Content-Type": "application/json",
    "Idempotency-Key": "order-789-shipment-2"
}

payload = {
    "capture_amount": 35.00,
    "final_capture": True
}

response = requests.post(url, json=payload, headers=headers)

print(response.status_code)
print(response.json())
```

```nodejs
const fetch = require("node-fetch");

const url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/captures";

const payload = {
  capture_amount: 35.00,
  final_capture: true
};

fetch(url, {
  method: "POST",
  headers: {
    "Authorization": "Bearer <YOUR_API_TOKEN>",
    "Content-Type": "application/json",
    "Idempotency-Key": "order-789-shipment-2"
  },
  body: JSON.stringify(payload)
})
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));
```
:::

```json
{
  "identifier": "debit_123",
  "state": "preauthorized",
  "capture_id": "capture_002",
  "capture_status": "paid",
  "capture_amount": 35.00,
  "principal_captured": 75.00,
  "captured_amount": 77.25,
  "available_amount": 25.00,
  "final_capture": true,
  "release": {
    "requested": true,
    "amount": 25.00,
    "state": "pending",
    "reason": null
  }
}
```

To capture the full amount in one step, call the same endpoint with no body.

### Release the remaining balance

Endpoint: [`POST /debit/{debit_id}/release`](https://extapi.jetpay.baselinepayments.com/docs#tag/Transactions-\(v0\)/operation/release_preauth_debit__transaction_id__release_post)

If you won't capture anything more and didn't send a final capture, release the rest of the hold.

:::CodeblockTabs
```curl
curl https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/release \
  -X POST \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Idempotency-Key: order-789-release"
```

```python
import requests

url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/release"

headers = {
    "Authorization": "Bearer <YOUR_API_TOKEN>",
    "Idempotency-Key": "order-789-release"
}

response = requests.post(url, headers=headers)

print(response.status_code)
print(response.json())
```

```nodejs
const fetch = require("node-fetch");

const url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/release";

fetch(url, {
  method: "POST",
  headers: {
    "Authorization": "Bearer <YOUR_API_TOKEN>",
    "Idempotency-Key": "order-789-release"
  }
})
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));
```
:::

```json
{
  "identifier": "debit_123",
  "state": "release_requested",
  "released_amount": 60.00,
  "principal_captured": 40.00,
  "captured_amount": 41.20,
  "available_amount": 0.00,
  "capture_count": 1,
  "release": null
}
```

### List captures

Endpoint: [`GET /debit/{debit_id}/captures`](https://extapi.jetpay.baselinepayments.com/docs#tag/Transactions-\(v0\)/operation/get_debit_captures_debit__transaction_id__captures_get)

See every capture on a debit, along with its running totals. To fetch one capture, use [`GET /debit/{debit_id}/captures/{capture_id}`](https://extapi.jetpay.baselinepayments.com/docs#tag/Transactions-\(v0\)/operation/get_debit_capture_debit__transaction_id__captures__capture_id__get).

:::CodeblockTabs
```curl
curl https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/captures \
  -H "Authorization: Bearer <YOUR_API_TOKEN>"
```

```python
import requests

url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/captures"

headers = {
    "Authorization": "Bearer <YOUR_API_TOKEN>"
}

response = requests.get(url, headers=headers)

print(response.status_code)
print(response.json())
```

```nodejs
const fetch = require("node-fetch");

const url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/captures";

fetch(url, {
  method: "GET",
  headers: {
    "Authorization": "Bearer <YOUR_API_TOKEN>"
  }
})
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));
```
:::

```json
{
  "transaction_id": "debit_123",
  "currency": "CAD",
  "principal_amount": 100.00,
  "preauthorized_amount": 103.00,
  "principal_captured": 40.00,
  "captured_amount": 41.20,
  "available_amount": 60.00,
  "released_amount": 0.00,
  "capture_count": 1,
  "state": "preauthorized",
  "is_capturable": true,
  "captures": [
    {
      "capture_id": "capture_001",
      "capture_status": "paid",
      "capture_amount": 40.00,
      "customer_surcharge_amount": 1.20,
      "total_charged_amount": 41.20,
      "created_at": "2026-09-28T15:04:05Z",
      "estimated_deposit_date": "2026-09-30",
      "is_final": false
    }
  ]
}
```

### Track status updates via events/webhooks

As captures and the preauthorization move through their lifecycle, an **Event** is created for each update. The most efficient way to track these updates in your system is to register for webhooks. If you are not yet familiar with our webhooks system, please review the [Events and webhooks](docId:79a4CGVETAChOLkpJYd5C) guide.

See the [Transactions reference](https://extapi.jetpay.baselinepayments.com/docs#tag/Transactions-\(v0\)) for request fields, error codes and limits, and the [Events reference](https://extapi.jetpay.baselinepayments.com/docs#section/Events-Reference) for the capture and preauthorization events.

***

## Handling failures

**The preauthorization is declined.** The request returns `422` and no debit is created. Ask the customer for another card, or try the same card again later.

**A capture is refused.** Nothing was captured. The `error` code in the response tells you why, and the fix is usually one of these:

- The amount is more than what's still available. [List captures](#list-captures) to get the current balance.
- Partial captures aren't enabled on your account. Capture the full amount, or contact [Jetpay support](mailto\:jetpay@baselinepayments.com).
- The preauthorization has hit its capture limit, already has a final capture, has been released, or has expired. It can't take more captures. Release what's left, or charge the card again with a new payment.

**A capture request times out or returns a&#x20;**`5xx`**.** Retry with the **same** `Idempotency-Key`. If the first attempt went through, you get the original capture back instead of a second one.

**The&#x20;**`Idempotency-Key`**&#x20;is missing.** A capture with `capture_amount` or `final_capture` but no `Idempotency-Key` header is refused with a `400`. Add the header and send it again.

**The&#x20;**`Idempotency-Key`**&#x20;is reused for a different capture.** Each key belongs to the request it was first used with. Sending the same key with a different body is refused:

```javascript
POST /debit/debit_123/captures
Idempotency-Key: order-789-shipment-1
{ "capture_amount": 40.00 }
→ 201, capture_001

POST /debit/debit_123/captures
Idempotency-Key: order-789-shipment-1
{ "capture_amount": 35.00 }
→ 422
{
  "error": "idempotencyKeyMismatch",
  "message": "Idempotency-Key was reused with a different request body."
}
```

Use a new key for every new capture, and reuse a key only when retrying the exact same request.

**A capture fails after it was accepted.** You receive `payments.captures.failed`. The failed amount becomes available again, so you can retry it as a new capture with a new `Idempotency-Key`. Handle this event even if you never received `payments.captures.created` for that capture.

**A final capture fails.** The pending release is cancelled and the preauthorization reopens. Capture again, or release the balance yourself.

**A release doesn't go through.** The debit returns to `preauthorized` and no event is sent. If you haven't received `payments.preauthorizations.released` within the time you expect, check the debit's state and call release again with a new `Idempotency-Key`.

**The preauthorization expires.** You receive `payments.preauthorizations.expired`. Anything you already captured is kept, and the rest is freed. To collect more, charge the card again.

***

## Testing

Use the demo environment to run the full flow before going live. Partial captures are off by default, so ask [Jetpay support](mailto\:jetpay@baselinepayments.com) to enable them on your demo account first.

1. Preauthorize a card, then capture part of the amount.
2. Send the same capture again with the same `Idempotency-Key` and confirm you get the same `capture_id` back.
3. Try to capture more than is available and confirm you handle the refusal.
4. Make a final capture, then confirm you receive `payments.preauthorizations.released`.
5. Preauthorize again and release the whole amount without capturing.

Declined captures, failed releases and expiry can't be triggered on demand, so cover those paths with unit tests against the example events.

***

## Best practices

- **Send an&#x20;**`Idempotency-Key`**&#x20;on every capture and release.** It's required for partial and final captures, and it makes every retry safe. Reuse the key when retrying the same request, and use a new key for each new capture.
- **Finish with a final capture.** It frees the customer's remaining balance as soon as you're done, instead of holding it until expiry.
- **Capture well before expiry.** A preauthorization lasts about 7 days. Don't plan captures for the last day.
- **Don't hard-code the capture limit.** Read `is_capturable` from [List captures](#list-captures) to know whether another capture will be accepted.
- **Store both IDs.** Keep the `debit_id` and every `capture_id` so you can match webhook events to your records.
- **Drive your status from webhooks.** Update your records from events rather than polling.

***

## FAQ

**How long does a preauthorization last?**
About 7 days. After that it expires and any uncaptured amount is freed.

**Can I capture more than I preauthorized?**
No. Captures can add up to the preauthorized amount at most. To charge more, create a new payment.

**Do I need partial captures enabled to use this flow?**
Only for partial captures. Capturing the full amount in one step works without it.

**How is the customer surcharge handled across captures?**
Each capture charges its share of the surcharge, so capturing the full amount charges exactly the preauthorized total.

**What if I never capture?**
The preauthorization expires on its own. Release it as soon as you know you won't capture, so the customer's funds aren't held longer than needed.

**Can I refund a preauthorization?**
You can refund what has been captured, even while the rest is still open. See [Refund a credit card transaction](docId\:yNtgRASa-IrocK2wDo79u). To give back an amount you haven't captured, release it.

**What happens if I send two captures at the same time?**
Both are checked against the same balance, so you can never capture more than the preauthorized amount. If the second one would exceed it, it's refused.
