On this page
No Headings
Last updated: July 8, 2026
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:
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?
Add the following to your existing server file from the quick start integration.
# 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 '{}'// 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 });
}
}
});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)}), 500import 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
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');
}
});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 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."}// 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."}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."}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
// 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."}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."}AUTHORIZE intent. Store this value in your database immediately.AUTHORIZE intent.| 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 |
For negative testing:
.env file, set ENABLE_NEGATIVE_TESTING=true and set NEGATIVE_TEST_TYPE to one of the error codes in the table..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 |
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. |