Set Up and Secure Webhooks for PayPal Checkout
Integrate PayPal webhooks to accept online payments with PayPal Checkout, and receive real-time notifications for payment events, such as completed payments, refunds, and subscription renewals. This guide shows you how to set up, secure, and test PayPal webhooks for your online payments integration.
What Are PayPal Webhooks?
Webhooks are HTTPS POST
requests that PayPal sends to your server when important events occur, such as when a payment is captured or a subscription is renewed. Set up your system to respond automatically to changes without polling PayPal’s API.
Prerequisites
- A PayPal Business account.
- Access to the PayPal Developer Dashboard.
- A server endpoint URL that can receive HTTPS
POST
requests.
1. Subscribe to webhook events
- Log in to the PayPal Developer Dashboard.
- Go to My Apps & Credentials.
- Select your app or create a new one.
- In your app details, scroll to the Webhooks section.
- Click Add Webhook.
- Enter your webhook listener URL, such as the endpoint on your server.
- Select the event types you want to receive, such as
PAYMENT.CAPTURE.COMPLETED
,CHECKOUT.ORDER.APPROVED
, orBILLING.SUBSCRIPTION.CREATED
. You can choose “All Events” or only those you need. - Click Save. Your webhook is now active.
2. Implement your webhook listener
Create an HTTPS endpoint on your server to receive POST
requests from PayPal. Review the following examples:
- cURL
- Python
- JavaScript (Node.js)
- TypeScript
- Java
- .NET (C#)
- Ruby
- PHP
curl -X POST \
https://your-server.com/webhook/paypal \
-H 'Content-Type: application/json' \
-d '{
"event_type": "PAYMENT.CAPTURE.COMPLETED",
"resource": {
"id": "your_capture_id",
"amount": {
"value": "10.00",
"currency": "USD"
}
}
}'
import requests
import json
url = 'https://your-server.com/webhook/paypal'
headers = {'Content-Type': 'application/json'}
data = {
"event_type": "PAYMENT.CAPTURE.COMPLETED",
"resource": {
"id": "your_capture_id",
"amount": {
"value": "10.00",
"currency": "USD"
}
}
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
print("Webhook processed successfully")
else:
print(f"Error processing webhook: {response.status_code}")
app.post('/webhook/paypal', (req, res) => {
const event = req.body;
// TODO: Verify webhook signature (see next section)
// Handle event types (e.g., payment completed, subscription renewed)
res.sendStatus(200);
});
import fetch from 'node-fetch'; // Ensure you have node-fetch installed (`npm install node-fetch`)
async function sendWebhook() {
const url = 'https://your-server.com/webhook/paypal';
const headers = { 'Content-Type': 'application/json' };
const body = JSON.stringify({
event_type: 'PAYMENT.CAPTURE.COMPLETED',
resource: {
id: 'your_capture_id',
amount: {
value: '10.00',
currency: 'USD',
},
},
});
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: body,
});
if (response.ok) {
console.log('Webhook sent successfully');
} else {
console.error(`Error sending webhook: ${response.status}`);
}
} catch (error) {
console.error('Error sending webhook:', error);
}
}
sendWebhook();
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WebhookHandler {
public static void main(String[] args) throws IOException, InterruptedException {
String url = "https://your-server.com/webhook/paypal";
String jsonPayload = "{\"event_type\": \"PAYMENT.CAPTURE.COMPLETED\", \"resource\": {\"id\": \"your_capture_id\", \"amount\": {\"value\": \"10.00\", \"currency\": \"USD\"}}}";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
System.out.println("Webhook processed successfully");
} else {
System.out.println("Error processing webhook: " + response.statusCode());
}
}
}
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
public class WebhookSender
{
public static async Task SendWebhookAsync()
{
string url = "https://your-server.com/webhook/paypal";
string jsonPayload = @"{
""event_type"": ""PAYMENT.CAPTURE.COMPLETED"",
""resource"": {
""id"": ""your_capture_id"",
""amount"": {
""value"": ""10.00"",
""currency"": ""USD""
}
}
}";
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
StringContent content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.PostAsync(url, content);
if (response.IsSuccessStatusCode)
{
Console.WriteLine("Webhook sent successfully");
}
else
{
Console.WriteLine($"Error sending webhook: {response.StatusCode}");
}
}
}
}
require 'uri'
require 'net/http'
require 'json'
url = URI('https://your-server.com/webhook/paypal')
header = {'Content-Type': 'application/json'}
data = {
'event_type': 'PAYMENT.CAPTURE.COMPLETED',
'resource': {
'id': 'your_capture_id',
'amount': {
'value': '10.00',
'currency': 'USD'
}
}
}
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url.to_s, header)
request.body = data.to_json
response = http.request(request)
puts response.read_body
<?php
$url = 'https://your-server.com/webhook/paypal';
$data = array(
'event_type' => 'PAYMENT.CAPTURE.COMPLETED',
'resource' => array(
'id' => 'your_capture_id',
'amount' => array(
'value' => '10.00',
'currency' => 'USD'
)
)
);
$data_string = json_encode($data);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'Content-Length: ' . strlen($data_string))
);
$result = curl_exec($ch);
if ($result === false) {
echo 'Curl error: ' . curl_error($ch);
} else {
echo $result;
}
curl_close($ch);
?>
3. Secure your webhook endpoint
Use HTTPS
Always use HTTPS for your webhook endpoint to encrypt data in transit.
Validate incoming webhook requests
-
Extract headers from PayPal’s request:
paypal-transmission-id
paypal-transmission-time
paypal-transmission-sig
paypal-cert-url
paypal-auth-algo
webhook-id
(from your dashboard)
-
Verify the signature using the Verify webhook signature endpoint of the Webhooks Management API:
- Send the headers and raw request body to PayPal’s verification endpoint.
- Only process the event if verification succeeds.
-
Check event details, such as transaction amount and payer info, against your records to prevent fraud.
Additional security best practices
- Idempotency: Ensure your webhook processing logic is idempotent to avoid duplicate handling.
- Logging: Log all received events for auditing and troubleshooting.
- Restrict access: Only allow PayPal’s IPs if possible, and never expose sensitive data in responses.
- Keep your SDKs and dependencies up to date.
4. Test your webhook integration
- Use PayPal’s webhook simulator in the Developer Dashboard to send test events to your endpoint and verify your handler works as expected.
- For local development, use a tunneling tool like ngrok to expose your local server to PayPal.
5. Monitor and maintain
- Monitor logs for unexpected or repeated webhook calls.
- Update your webhook subscriptions as your integration evolves.
- Regularly review PayPal’s developer updates for security changes.
You now have a secure, reliable webhook integration for PayPal Checkout. This ensures your system stays in sync with real payment events, automatically and safely.
Additional webhook endpoints
Use the following endpoints to monitor and manage your webhooks:
- List Webhooks: Return a list of existing webhooks by sending a
GET
request to the List webhooks endpoint. - Show Webhook Details: Return details about a specific webhook by sending a
GET
request to the Show webhook details endpoint. - Delete Webhook: Delete a specific webhook by sending its webhook ID in a
DELETE
request to the Delete webhook endpoint. - Update Webhook: Update a particular webhook by sending a
PATCH
request with its webhook ID to the Update webhook endpoint. Supports only thereplace
operation.