Skip to main content
Charge a customer after fulfilling their order, not immediately. Common use cases include:
  • Reserve payment for pre-orders shipping in the future
  • Charge after made-to-order items are produced
  • Authorize hotels or rentals at booking, capture at check-in or check-out
  • Confirm authenticity of high value items requiring verification before capturing payment
This integration uses the Orders API v2 to authorize and the Payments API v2 to capture later. To authorize first and capture later, change your integration to use intent: "AUTHORIZE". This reserves funds on the customer’s payment method for up to 29 days. The highest success rate is within the first 3 days (the honor period). After 3 days, you may use reauthorization to extend the hold. What’s the difference between authorization and capture?
Before you begin, you’ll need to complete the quick start PayPal integration.

Integrate server side

Add the following to your existing server file from the quick start integration.
# Step 1: Create order with AUTHORIZE intent
curl -X POST https://api-m.sandbox.paypal.com/v2/checkout/orders \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -d '{
    "intent": "AUTHORIZE",
    "purchase_units": [{
      "amount": {
        "currency_code": "USD",
        "value": "100.00"
      }
    }]
  }'

# Step 2: Capture the authorization later
curl -X POST https://api-m.sandbox.paypal.com/v2/payments/authorizations/AUTHORIZATION_ID/capture \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -d '{}'
// Add these endpoints to your existing server.js from the quick start integration
// The PayPal client and addNegativeTesting helper are already configured

// Modify the create order endpoint to use AUTHORIZE intent
app.post('/api/orders', async (req, res) => {
  const request = new paypal.orders.OrdersCreateRequest();
  request.requestBody({
    intent: 'AUTHORIZE', // Changed from 'CAPTURE'
    purchase_units: [{
      amount: {
        currency_code: 'USD',
        value: req.body.amount || '100.00'
      }
    }]
  });

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

  try {
    const order = await client.execute(request);
    res.json({ id: order.result.id });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Add new endpoint to capture the authorization later
app.post('/api/orders/:orderId/capture', async (req, res) => {
  const { authorizationId } = req.body;
  const request = new paypal.payments.AuthorizationsCaptureRequest(authorizationId);

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

  try {
    const capture = await client.execute(request);
    res.json({
      status: capture.result.status,
      captureId: capture.result.id
    });
  } catch (err) {
    if (err.statusCode === 422) {
      res.status(400).json({ error: 'Authorization expired or invalid' });
    } else {
      res.status(500).json({ error: err.message });
    }
  }
});
from paypalrestsdk import Api, Order, Authorization
import os

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

# Create order with AUTHORIZE intent
@app.route('/api/orders', methods=['POST'])
def create_order():
  try:
    order = Order({
      "intent": "AUTHORIZE",
      "purchase_units": [{
        "amount": {
          "currency_code": "USD",
          "value": request.json.get('amount', '100.00')
        }
      }]
    })

    if order.create():
      return jsonify({"id": order.id})
    else:
      return jsonify({"error": order.error}), 500

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

# Capture authorization later
@app.route('/api/orders/<order_id>/capture', methods=['POST'])
def capture_authorization(order_id):
  try:
    authorization_id = request.json.get('authorizationId')
    authorization = Authorization.find(authorization_id)

    if authorization.capture():
      return jsonify({
        "status": authorization.state,
        "captureId": authorization.id
      })
    else:
      error = authorization.error
      if error.get('name') == 'AUTHORIZATION_EXPIRED':
        return jsonify({
          "error": "Authorization expired or invalid"
        }), 400
      else:
        return jsonify({"error": error.get('message')}), 500

  except Exception as e:
    return jsonify({"error": str(e)}), 500
import com.paypal.core.PayPalEnvironment;
import com.paypal.core.PayPalHttpClient;
import com.paypal.orders.*;
import com.paypal.payments.AuthorizationsCaptureRequest;
import com.paypal.payments.Capture;

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

// Create order with AUTHORIZE intent
@PostMapping("/api/orders")
public ResponseEntity<?> createOrder(@RequestBody OrderRequest orderRequest) {
  try {
    OrdersCreateRequest request = new OrdersCreateRequest();
    request.prefer("return=representation");
    request.body(new OrderRequest()
      .intent("AUTHORIZE")
      .purchaseUnits(List.of(
        new PurchaseUnitRequest()
          .amount(new Money()
            .currencyCode("USD")
            .value(orderRequest.getAmount() != null ?
              orderRequest.getAmount() : "100.00")
          )
      ))
    );

    HttpResponse<Order> response = httpClient.execute(request);
    return ResponseEntity.ok(Map.of("id", response.result().id()));

  } catch (Exception e) {
    return ResponseEntity.status(500).body(
      Map.of("error", e.getMessage())
    );
  }
}

// Capture authorization later
@PostMapping("/api/orders/{orderId}/capture")
public ResponseEntity<?> captureAuthorization(
  @PathVariable String orderId,
  @RequestBody Map<String, String> requestBody
) {
  try {
    String authorizationId = requestBody.get("authorizationId");
    AuthorizationsCaptureRequest request = new AuthorizationsCaptureRequest(authorizationId);

    HttpResponse<Capture> response = httpClient.execute(request);
    Capture capture = response.result();

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

  } catch (HttpClientException e) {
    if (e.statusCode() == 422) {
      return ResponseEntity.badRequest().body(
        Map.of("error", "Authorization expired or invalid")
      );
    }
    return ResponseEntity.status(500).body(
      Map.of("error", e.getMessage())
    );
  }
}
<?php
use PayPalCheckoutSdk\Core\SandboxEnvironment;
use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Orders\OrdersCreateRequest;
use PayPalCheckoutSdk\Payments\AuthorizationsCaptureRequest;

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

// Create order with AUTHORIZE intent
$app->post('/api/orders', function ($request, $response) use ($client) {
  $body = $request->getParsedBody();

  $orderRequest = new OrdersCreateRequest();
  $orderRequest->prefer("return=representation");
  $orderRequest->body = [
    "intent" => "AUTHORIZE",
    "purchase_units" => [[
      "amount" => [
        "currency_code" => "USD",
        "value" => $body['amount'] ?? "100.00"
      ]
    ]]
  ];

  try {
    $orderResponse = $client->execute($orderRequest);
    $response->getBody()->write(json_encode([
      "id" => $orderResponse->result->id
    ]));
    return $response->withHeader('Content-Type', 'application/json');

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

// Capture authorization later
$app->post('/api/orders/{orderId}/capture', function ($request, $response, $args) use ($client) {
  $body = $request->getParsedBody();
  $authorizationId = $body['authorizationId'];

  $captureRequest = new AuthorizationsCaptureRequest($authorizationId);

  try {
    $captureResponse = $client->execute($captureRequest);
    $capture = $captureResponse->result;

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

  } catch (HttpException $e) {
    if ($e->statusCode === 422) {
      $response->getBody()->write(json_encode([
        "error" => "Authorization expired or invalid"
      ]));
      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']
)

# Create order with AUTHORIZE intent
post '/api/orders' do
  request_body = JSON.parse(request.body.read)

  begin
    order = PayPal::SDK::OrdersApi::Order.new(
      intent: "AUTHORIZE",
      purchase_units: [{
        amount: {
          currency_code: "USD",
          value: request_body['amount'] || "100.00"
        }
      }]
    )

    if order.create
      content_type :json
      { id: order.id }.to_json
    else
      status 500
      { error: order.error }.to_json
    end

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

# Capture authorization later
post '/api/orders/:order_id/capture' do
  order_id = params['order_id']
  request_body = JSON.parse(request.body.read)
  authorization_id = request_body['authorizationId']

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

    if authorization.capture
      content_type :json
      {
        status: authorization.status,
        captureId: authorization.id
      }.to_json
    else
      error = authorization.error
      if error['name'] == 'AUTHORIZATION_EXPIRED'
        status 400
        { error: 'Authorization expired or invalid' }.to_json
      else
        status 500
        { error: error['message'] }.to_json
      end
    end

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

Test endpoints

# 1. Create authorization (returns orderId)
curl -X POST http://localhost:3000/api/orders \
  -H "Content-Type: application/json" \
  -d '{"amount": "100.00"}'

# Expected response:
# {"id":"5O190127TN364715T"}

# 2. Later, capture the authorization (use authorizationId from PayPal)
curl -X POST http://localhost:3000/api/orders/ORDER_ID/capture \
  -H "Content-Type: application/json" \
  -d '{"authorizationId": "AUTH_ID_FROM_PAYPAL"}'

# Expected response:
# {"status":"COMPLETED","captureId":"3C679366HH908993F"}
// 1. Create authorization
const createResponse = await fetch('http://localhost:3000/api/orders', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ amount: '100.00' })
});
const order = await createResponse.json();
console.log('Order created:', order);
// Expected: {"id":"5O190127TN364715T"}

// 2. Later, capture the authorization
const captureResponse = await fetch('http://localhost:3000/api/orders/ORDER_ID/capture', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ authorizationId: 'AUTH_ID_FROM_PAYPAL' })
});
const capture = await captureResponse.json();
console.log('Capture result:', capture);
// Expected: {"status":"COMPLETED","captureId":"3C679366HH908993F"}
import requests

# 1. Create authorization
create_response = requests.post(
  'http://localhost:3000/api/orders',
  headers={'Content-Type': 'application/json'},
  json={'amount': '100.00'}
)
print('Order created:', create_response.json())
# Expected: {"id":"5O190127TN364715T"}

# 2. Later, capture the authorization
capture_response = requests.post(
  'http://localhost:3000/api/orders/ORDER_ID/capture',
  headers={'Content-Type': 'application/json'},
  json={'authorizationId': 'AUTH_ID_FROM_PAYPAL'}
)
print('Capture result:', capture_response.json())
# Expected: {"status":"COMPLETED","captureId":"3C679366HH908993F"}
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;

HttpClient client = HttpClient.newHttpClient();

// 1. Create authorization
String createBody = "{\"amount\":\"100.00\"}";
HttpRequest createRequest = HttpRequest.newBuilder()
  .uri(URI.create("http://localhost:3000/api/orders"))
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.ofString(createBody))
  .build();

HttpResponse<String> createResponse = client.send(
  createRequest,
  HttpResponse.BodyHandlers.ofString()
);
System.out.println("Order created: " + createResponse.body());
// Expected: {"id":"5O190127TN364715T"}

// 2. Later, capture the authorization
String captureBody = "{\"authorizationId\":\"AUTH_ID_FROM_PAYPAL\"}";
HttpRequest captureRequest = HttpRequest.newBuilder()
  .uri(URI.create("http://localhost:3000/api/orders/ORDER_ID/capture"))
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.ofString(captureBody))
  .build();

HttpResponse<String> captureResponse = client.send(
  captureRequest,
  HttpResponse.BodyHandlers.ofString()
);
System.out.println("Capture result: " + captureResponse.body());
// Expected: {"status":"COMPLETED","captureId":"3C679366HH908993F"}
<?php
// 1. Create authorization
$createData = json_encode(['amount' => '100.00']);

$createResponse = file_get_contents(
  'http://localhost:3000/api/orders',
  false,
  stream_context_create([
    'http' => [
      'method' => 'POST',
      'header' => 'Content-Type: application/json',
      'content' => $createData
    ]
  ])
);
echo "Order created: " . $createResponse . "\n";
// Expected: {"id":"5O190127TN364715T"}

// 2. Later, capture the authorization
$captureData = json_encode(['authorizationId' => 'AUTH_ID_FROM_PAYPAL']);

$captureResponse = file_get_contents(
  'http://localhost:3000/api/orders/ORDER_ID/capture',
  false,
  stream_context_create([
    'http' => [
      'method' => 'POST',
      'header' => 'Content-Type: application/json',
      'content' => $captureData
    ]
  ])
);
echo "Capture result: " . $captureResponse . "\n";
// Expected: {"status":"COMPLETED","captureId":"3C679366HH908993F"}
require 'net/http'
require 'json'

# 1. Create authorization
uri = URI('http://localhost:3000/api/orders')
create_request = Net::HTTP::Post.new(uri)
create_request['Content-Type'] = 'application/json'
create_request.body = { amount: '100.00' }.to_json

create_response = Net::HTTP.start(uri.hostname, uri.port) do |http|
  http.request(create_request)
end
puts "Order created: #{create_response.body}"
# Expected: {"id":"5O190127TN364715T"}

# 2. Later, capture the authorization
capture_uri = URI('http://localhost:3000/api/orders/ORDER_ID/capture')
capture_request = Net::HTTP::Post.new(capture_uri)
capture_request['Content-Type'] = 'application/json'
capture_request.body = { authorizationId: 'AUTH_ID_FROM_PAYPAL' }.to_json

capture_response = Net::HTTP.start(capture_uri.hostname, capture_uri.port) do |http|
  http.request(capture_request)
end
puts "Capture result: #{capture_response.body}"
# Expected: {"status":"COMPLETED","captureId":"3C679366HH908993F"}

Best practices

  • Capture within 3-day honor period: Authorization success rates are highest during the 3-day honor period. After the payment is authorized, ship within 3 days.
  • Track authorization expiration: Store the date when the authorization was created in your database. Alert when approaching day 29 and have a process to handle expiring authorizations.
  • Handle partial captures: You can capture any amount up to the authorized total. The remainder can be voided or left to expire. For multiple shipments, use multiple partial captures.
  • Void uncaptured authorizations promptly: If you can’t fulfill an order, void the authorization immediately to release customer funds.
  • Communicate holds to customers: Inform customers that their payment is authorized, but not yet charged. Inform them when the actual charge will occur.
  • Monitor authorization validity: Authorizations can be voided by the customer’s bank or PayPal. Handle capture failures gracefully.

Important details

  • Find authorization IDs: The authorization ID is in the order approval response at purchase_units[0].payments.authorizations[0].id. Store this value in your database immediately.
  • Authorization expiration: You have up to 29 days to capture payment. Captures are most likely to succeed during the 3-day honor period. After 29 days, the authorization automatically expires, and you’ll need to create a new order to charge the customer.
  • Partial captures: You can capture any amount up to the authorized total.
  • Extending authorizations: Use reauthorization between days 4-29 to extend an authorization. See Reauthorize an authorization.

Test your integration

Sandbox authorizations don’t expire after 29 days. Test authorizations with negative testing instead.

Standard testing

Test scenarioSetupExpected result
Authorize and captureCreate an order with intent: "AUTHORIZE", then capture after 1 minuteAuthorization successful, capture completes
Partial captureAuthorize $100, capture $60$60 captured, $40 remains available
Invalid authorization IDUse "INVALID_AUTH_123" as auth IDReturns 404 error
Capture after 5 daysWait 5 days past honor periodCapture may succeed with lower rate

Negative testing

  1. Make sure to enable negative testing in your sandbox business account as described in the quick start prerequisites.
  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 scenarioError codeExpected result
Expired authorizationAUTHORIZATION_EXPIREDReturns 422 error with “Authorization expired or invalid”
Already capturedAUTHORIZATION_ALREADY_CAPTUREDReturns 422 error with “Authorization already captured”

Go-live checklist

  • Implement authorization tracking system.
  • Integrate capture process with fulfillment workflow.
  • Configure authorization expiration monitoring.
  • Test customer communication about holds.
  • Test with real $1 authorization and capture.

Post-launch monitoring

These values are suggested monitoring thresholds for your integration, not performance guarantees from PayPal.
MetricTargetAction if below target
Authorization success rate95%Investigate authorization failures with issuing banks
Capture rate within 3 days90%Optimize fulfillment workflow for faster processing
Authorization expiration rate<2%Improve expiration alerts and capture automation
API response time<2 secondsCheck PayPal API status
Capture success rate98%Review expired or voided authorizations