# Void an authorized payment (/checkout/void-authorized-payment)

Cancel an authorization before capture.



Cancel an authorized payment before capturing it to release the customer's held funds. Voiding an authorized payment saves processing fees compared to refunding a captured payment. Common scenarios include:

* Customer cancels an order before shipment
* Item is out of stock after authorization
* Order fails fraud verification
* Customer requests cancellation during fulfillment

Add the void endpoint to cancel authorizations. This integration uses the Payments API v2 and requires an authorized payment that hasn't been captured yet.

[What's the difference between authorization and capture?](/v5/checkout/auth-capture)

## Prerequisites [#prerequisites]

* Complete [the quick start PayPal integration](/checkout/integrate).
* Implement an authorization and capture flow that doesn't use immediate capture.
* Have an authorization ID from an order creation. Store authorization IDs in your database when creating orders. You'll need them to void the authorization.
* Optional for production:
  * Automated void triggers for inventory and fraud checks
  * Customer notifications on voided payments
  * Void reason tracking for analytics

## Integrate server side [#integrate-server-side]

Add the following to your existing server file from the quick start integration.

#### cURL

```bash
# Void an authorization
curl -X POST https://api-m.sandbox.paypal.com/v2/payments/authorizations/AUTHORIZATION_ID/void \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -d '{}'
```

#### Node.js

```javascript
// Add this endpoint to your existing server.js from the quick start integration
// The PayPal client and addNegativeTesting helper are already configured

// Void an authorization (cancel before capture)
app.post('/api/authorizations/:authID/void', async (req, res) => {
  const request = new paypal.payments.AuthorizationsVoidRequest(req.params.authID);

  // Apply negative testing if enabled (reuse helper from base integration)
  addNegativeTesting(request);

  try {
    const voidResult = await client.execute(request);
    res.json({
      id: voidResult.result.id,
      status: voidResult.result.status
    });
  } catch (err) {
    // Handle specific void errors
    if (err.statusCode === 422) {
      const errorDetail = err.details?.[0];
      if (errorDetail?.issue === 'AUTHORIZATION_ALREADY_CAPTURED') {
        res.status(400).json({
          error: 'Cannot void - already captured. Use refund instead.',
          captureId: errorDetail.description
        });
      } else if (errorDetail?.issue === 'AUTHORIZATION_VOIDED') {
        res.status(400).json({ error: 'Authorization already voided' });
      } else {
        res.status(400).json({ error: 'Invalid authorization state' });
      }
    } else {
      res.status(500).json({ error: err.message });
    }
  }
});
```

#### Python

```python
from paypalrestsdk import Api, Authorization
import os

# Configure PayPal SDK
api = Api({
  'mode': 'sandbox',
  'client_id': os.environ['PAYPAL_CLIENT_ID'],
  'client_secret': os.environ['PAYPAL_CLIENT_SECRET']
})

# Void an authorization
@app.route('/api/authorizations/<auth_id>/void', methods=['POST'])
def void_authorization(auth_id):
  try:
    authorization = Authorization.find(auth_id)

    if authorization.void():
      return jsonify({
        "id": authorization.id,
        "status": authorization.state
      })
    else:
      # Handle errors
      error = authorization.error
      if error.get('name') == 'AUTHORIZATION_ALREADY_CAPTURED':
        return jsonify({
          "error": "Cannot void - already captured. Use refund instead."
        }), 400
      elif error.get('name') == 'AUTHORIZATION_VOIDED':
        return jsonify({
          "error": "Authorization already voided"
        }), 400
      else:
        return jsonify({
          "error": "Invalid authorization state"
        }), 400

  except Exception as e:
    return jsonify({"error": str(e)}), 500
```

#### Java

```java
import com.paypal.core.PayPalEnvironment;
import com.paypal.core.PayPalHttpClient;
import com.paypal.payments.AuthorizationsVoidRequest;
import com.paypal.payments.Authorization;

HttpClient httpClient = new PayPalHttpClient(
  new SandboxEnvironment(
    System.getenv("PAYPAL_CLIENT_ID"),
    System.getenv("PAYPAL_CLIENT_SECRET")
  )
);

// Void an authorization
@PostMapping("/api/authorizations/{authId}/void")
public ResponseEntity<?> voidAuthorization(@PathVariable String authId) {
  try {
    AuthorizationsVoidRequest request = new AuthorizationsVoidRequest(authId);

    HttpResponse<Authorization> response = httpClient.execute(request);
    Authorization authorization = response.result();

    return ResponseEntity.ok(Map.of(
      "id", authorization.id(),
      "status", authorization.status()
    ));

  } catch (HttpClientException e) {
    if (e.statusCode() == 422) {
      String issue = extractIssue(e);
      if ("AUTHORIZATION_ALREADY_CAPTURED".equals(issue)) {
        return ResponseEntity.badRequest().body(
          Map.of("error", "Cannot void - already captured. Use refund instead.")
        );
      } else if ("AUTHORIZATION_VOIDED".equals(issue)) {
        return ResponseEntity.badRequest().body(
          Map.of("error", "Authorization already voided")
        );
      } else {
        return ResponseEntity.badRequest().body(
          Map.of("error", "Invalid authorization state")
        );
      }
    }
    return ResponseEntity.status(500).body(
      Map.of("error", e.getMessage())
    );
  }
}
```

#### PHP

```php
<?php
use PayPalCheckoutSdk\Core\SandboxEnvironment;
use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Payments\AuthorizationsVoidRequest;

$environment = new SandboxEnvironment(
  getenv('PAYPAL_CLIENT_ID'),
  getenv('PAYPAL_CLIENT_SECRET')
);
$client = new PayPalHttpClient($environment);

// Void an authorization
$app->post('/api/authorizations/{authID}/void', function ($request, $response, $args) use ($client) {
  $authID = $args['authID'];

  $voidRequest = new AuthorizationsVoidRequest($authID);

  try {
    $voidResponse = $client->execute($voidRequest);
    $authorization = $voidResponse->result;

    $response->getBody()->write(json_encode([
      "id" => $authorization->id,
      "status" => $authorization->status
    ]));
    return $response->withHeader('Content-Type', 'application/json');

  } catch (HttpException $e) {
    $statusCode = $e->statusCode;
    $errorData = json_decode($e->getMessage(), true);

    if ($statusCode === 422) {
      $issue = $errorData['details'][0]['issue'] ?? '';
      if ($issue === 'AUTHORIZATION_ALREADY_CAPTURED') {
        $response->getBody()->write(json_encode([
          "error" => "Cannot void - already captured. Use refund instead."
        ]));
        return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
      } else if ($issue === 'AUTHORIZATION_VOIDED') {
        $response->getBody()->write(json_encode([
          "error" => "Authorization already voided"
        ]));
        return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
      } else {
        $response->getBody()->write(json_encode([
          "error" => "Invalid authorization state"
        ]));
        return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
      }
    }

    $response->getBody()->write(json_encode([
      "error" => $e->getMessage()
    ]));
    return $response->withStatus(500)->withHeader('Content-Type', 'application/json');
  }
});
```

#### Ruby

```ruby
require 'paypal-sdk'

# Configure PayPal SDK
PayPal::SDK.configure(
  :mode => "sandbox",
  :app_id => ENV['PAYPAL_APP_ID'],
  :client_id => ENV['PAYPAL_CLIENT_ID'],
  :client_secret => ENV['PAYPAL_CLIENT_SECRET']
)

# Void an authorization
post '/api/authorizations/:auth_id/void' do
  auth_id = params['auth_id']

  begin
    authorization = PayPal::SDK::PaymentsApi::Authorization.find(auth_id)

    if authorization.void
      content_type :json
      {
        id: authorization.id,
        status: authorization.status
      }.to_json
    else
      error = authorization.error
      if error['name'] == 'AUTHORIZATION_ALREADY_CAPTURED'
        status 400
        { error: 'Cannot void - already captured. Use refund instead.' }.to_json
      elsif error['name'] == 'AUTHORIZATION_VOIDED'
        status 400
        { error: 'Authorization already voided' }.to_json
      else
        status 400
        { error: 'Invalid authorization state' }.to_json
      end
    end

  rescue => e
    status 500
    { error: e.message }.to_json
  end
end
```

## Test endpoint [#test-endpoint]

> **Info:** Endpoint: [`/v2/payments/authorizations/{authorization_id}/void`](/api/payments/v2/authorizations-void/)

#### cURL

```bash
# Test voiding an authorization (replace with actual authorization ID)
curl -X POST http://localhost:3000/api/authorizations/8AA831015G517922L/void \
  -H "Content-Type: application/json"

# Expected success response:
# {"id":"8AA831015G517922L","status":"VOIDED"}

# Test voiding already captured authorization (should fail)
curl -X POST http://localhost:3000/api/authorizations/CAPTURED_AUTH_ID/void \
  -H "Content-Type: application/json"

# Expected error response:
# {"error":"Cannot void - already captured. Use refund instead."}
```

#### Node.js

```javascript
// Test voiding an authorization
const voidResponse = await fetch('http://localhost:3000/api/authorizations/8AA831015G517922L/void', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' }
});
const voidResult = await voidResponse.json();
console.log('Void result:', voidResult);
// Expected: {"id":"8AA831015G517922L","status":"VOIDED"}

// Test voiding already captured authorization (should fail)
const capturedVoidResponse = await fetch('http://localhost:3000/api/authorizations/CAPTURED_AUTH_ID/void', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' }
});
const capturedVoidResult = await capturedVoidResponse.json();
console.log('Error result:', capturedVoidResult);
// Expected: {"error":"Cannot void - already captured. Use refund instead."}
```

#### Python

```python
import requests

# Test voiding an authorization
void_response = requests.post(
  'http://localhost:3000/api/authorizations/8AA831015G517922L/void',
  headers={'Content-Type': 'application/json'}
)
print('Void result:', void_response.json())
# Expected: {"id":"8AA831015G517922L","status":"VOIDED"}

# Test voiding already captured authorization (should fail)
captured_void_response = requests.post(
  'http://localhost:3000/api/authorizations/CAPTURED_AUTH_ID/void',
  headers={'Content-Type': 'application/json'}
)
print('Error result:', captured_void_response.json())
# Expected: {"error":"Cannot void - already captured. Use refund instead."}
```

#### Java

```java
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;

HttpClient client = HttpClient.newHttpClient();

// Test voiding an authorization
HttpRequest voidRequest = HttpRequest.newBuilder()
  .uri(URI.create("http://localhost:3000/api/authorizations/8AA831015G517922L/void"))
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.noBody())
  .build();

HttpResponse<String> voidResponse = client.send(
  voidRequest,
  HttpResponse.BodyHandlers.ofString()
);
System.out.println("Void result: " + voidResponse.body());
// Expected: {"id":"8AA831015G517922L","status":"VOIDED"}

// Test voiding already captured authorization (should fail)
HttpRequest capturedVoidRequest = HttpRequest.newBuilder()
  .uri(URI.create("http://localhost:3000/api/authorizations/CAPTURED_AUTH_ID/void"))
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.noBody())
  .build();

HttpResponse<String> capturedVoidResponse = client.send(
  capturedVoidRequest,
  HttpResponse.BodyHandlers.ofString()
);
System.out.println("Error result: " + capturedVoidResponse.body());
// Expected: {"error":"Cannot void - already captured. Use refund instead."}
```

#### PHP

```php
<?php
// Test voiding an authorization
$voidResponse = file_get_contents(
  'http://localhost:3000/api/authorizations/8AA831015G517922L/void',
  false,
  stream_context_create([
    'http' => [
      'method' => 'POST',
      'header' => 'Content-Type: application/json'
    ]
  ])
);
echo "Void result: " . $voidResponse . "\n";
// Expected: {"id":"8AA831015G517922L","status":"VOIDED"}

// Test voiding already captured authorization (should fail)
$capturedVoidResponse = file_get_contents(
  'http://localhost:3000/api/authorizations/CAPTURED_AUTH_ID/void',
  false,
  stream_context_create([
    'http' => [
      'method' => 'POST',
      'header' => 'Content-Type: application/json'
    ]
  ])
);
echo "Error result: " . $capturedVoidResponse . "\n";
// Expected: {"error":"Cannot void - already captured. Use refund instead."}
```

#### Ruby

```ruby
require 'net/http'
require 'json'

# Test voiding an authorization
uri = URI('http://localhost:3000/api/authorizations/8AA831015G517922L/void')
void_request = Net::HTTP::Post.new(uri)
void_request['Content-Type'] = 'application/json'

void_response = Net::HTTP.start(uri.hostname, uri.port) do |http|
  http.request(void_request)
end
puts "Void result: #{void_response.body}"
# Expected: {"id":"8AA831015G517922L","status":"VOIDED"}

# Test voiding already captured authorization (should fail)
captured_uri = URI('http://localhost:3000/api/authorizations/CAPTURED_AUTH_ID/void')
captured_void_request = Net::HTTP::Post.new(captured_uri)
captured_void_request['Content-Type'] = 'application/json'

captured_void_response = Net::HTTP.start(captured_uri.hostname, captured_uri.port) do |http|
  http.request(captured_void_request)
end
puts "Error result: #{captured_void_response.body}"
# Expected: {"error":"Cannot void - already captured. Use refund instead."}
```

## Best practices [#best-practices]

* **Void promptly:** Cancel authorizations immediately when an order won't be fulfilled to release customer funds. Don't let an authorization expire.
* **Void before capture:** Always void instead of refund when possible. Voiding can save up to 3% in processing fees.
* **Automate cancellations:** Trigger voids automatically for out-of-stock scenarios or fraud detection.
* **Handle timing:** Authorizations expire after 3 days (standard) or 29 days (honor period).

## Important details [#important-details]

* **Void vs. refund:** Void cancels an authorization before capture. Refund returns money after a payment has been captured. Voiding saves processing fees.
* **Finding authorization IDs:** You need the authorization ID to void, not the order ID. The authorization ID is returned when you create an order with `AUTHORIZE` intent. Store this value in your database immediately.
* **All-or-nothing operation:** You cannot partially void an authorization. Voids cancel the entire authorized amount.
* **Voided authorizations cannot be captured:** Once voided, an authorization is permanently cancelled and cannot be captured. Attempting to capture will fail with an error.
* **Fund release time:** In sandbox mode, funds release immediately. In production, funds can take up to 24 hours depending on the customer's bank.

## Test your integration [#test-your-integration]

* Create test authorizations with `AUTHORIZE` intent.
* Test at different intervals: immediately after authorization, after 1 day, and after 3 days.
* Verify funds are released in the sandbox buyer account after voiding.

### Standard testing [#standard-testing]

| Test scenario            | Setup                          | Expected result                 |
| :----------------------- | :----------------------------- | :------------------------------ |
| Void fresh authorization | Default settings               | Status: `VOIDED` returned       |
| Void after capture       | Capture first                  | Error: Already captured         |
| Void already voided      | Void twice                     | Error: Already voided           |
| Void after 3 days        | Wait 3 days post-authorization | Success if honor period enabled |

### Negative testing [#negative-testing]

For negative testing:

1. Make sure to enable negative testing in your sandbox business account as described in the [quick start prerequisites](/checkout/integrate).
2. In the `.env` file, set `ENABLE_NEGATIVE_TESTING=true` and set `NEGATIVE_TEST_TYPE` to one of the error codes in the table.
3. Restart the server after changing the `.env` file: `node server.js`.

| Test scenario              | Error code              | Expected result                |
| :------------------------- | :---------------------- | :----------------------------- |
| Void expired authorization | `AUTHORIZATION_EXPIRED` | Error: Authorization expired   |
| Void not found             | `RESOURCE_NOT_FOUND`    | Error: Authorization not found |
| Permission denied          | `PERMISSION_DENIED`     | Error: not authorized to void  |
| Internal server error      | `INTERNAL_SERVER_ERROR` | 500 error returned             |

## Go-live checklist [#go-live-checklist]

* Test void flow in sandbox with real authorization IDs.
* Implement void reason tracking.
* Set up customer cancellation notifications.
* Train support team on void vs refund scenarios.
* Configure automatic void triggers, such as out of inventory or a fraudulent transaction.
* Monitor void success rates.
* Test with real $1 authorization and void it.

## Post-launch monitoring [#post-launch-monitoring]

These values are suggested monitoring thresholds for your integration, not performance guarantees from PayPal.

| Metric               | Target      | Action if below target             |
| :------------------- | :---------- | :--------------------------------- |
| Void success rate    | 99%         | Check for expired authorizations.  |
| Time to void         | \<1 hour    | Automate cancellation workflow.    |
| Void vs refund ratio | 80% void    | Train team to void before capture. |
| Failed void rate     | \<1%        | Review error patterns.             |
| API response time    | \<2 seconds | Check PayPal API status.           |
