Webhook Integration
Stay informed when events happen in your PayPal account. Set up webhooks to send notifications for events such as successful payments, failed transactions, or refunds.
This guide walks you through:
- Setting up a webhook listener.
- Subscribing to events.
- Verifying messages.
- Testing with PayPal's simulator.
Overview
Webhooks let your server listen for PayPal events in real time.
Common use cases:
- Confirming a payment was captured.
- Detecting failed transactions.
- Syncing order status in your database.
Common webhook events
Event Name | Description |
---|---|
PAYMENT.CAPTURE.COMPLETED | Payment completed successfully |
PAYMENT.CAPTURE.DENIED | Payment denied |
PAYMENT.CAPTURE.REFUNDED | Payment refunded |
See the full event list.
Webhook notification flow
- Webhook Setup Phase
- You register a URL through PayPal's Developer Dashboard
- PayPal confirms registration by sending you a webhook_id
- Transaction Phase
- Customer starts a payment on your site
- Your server creates a payment through PayPal
- Customer completes payment on PayPal
- PayPal sends confirmation to the customer
- Webhook Notification Phase
- PayPal sends a POST request to your webhook URL
- The request includes event data (like PAYMENT.CAPTURE.COMPLETED)
- PayPal adds special signature headers for security
- Verification Phase
- Your server extracts the PayPal signature headers
- Your server verifies the signature with PayPal
- If valid:
- Process the webhook event
- Update order status
- Return HTTP 200 OK
- If invalid:
- Log security warning
- Still return HTTP 200 OK (to acknowledge receipt)
- Retry Mechanism
- If your server doesn't respond with 200 OK
- PayPal retries the webhook delivery
- Retries continue for up to 3 days
1. Set up your webhook listener
Your listener should be a public HTTPS endpoint that accepts POST requests.
- cURL
- Python
- Node.js
- TypeScript
- Java
- .NET (C#)
- Ruby
- PHP
You can use curl
to simulate receiving a webhook by setting up a local server with tools like ngrok
or localtunnel
. Here's an example of how to send a test webhook to your listener:
curl -X POST https://your-webhook-endpoint.com/webhook \
-H "Content-Type: application/json" \
-d '{
"id": "WH-12345",
"event_type": "PAYMENT.CAPTURE.COMPLETED",
"resource": {
"id": "PAY-67890",
"status": "COMPLETED"
}
}'
Replace https://your-webhook-endpoint.com/webhook
with your actual webhook URL. This example sends a PAYMENT.CAPTURE.COMPLETED
event to your listener.
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
webhook_event = request.json
print("Received webhook:", webhook_event.get('event_type'))
return '', 200
app.post("/webhook", express.json(), async (req, res) => {
const webhookEvent = req.body;
// Optionally verify below
console.log("Received webhook:", webhookEvent.event_type);
res.sendStatus(200);
});
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
const webhookEvent = req.body;
console.log('Received webhook:', webhookEvent.event_type);
res.sendStatus(200);
});
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
@RestController
public class WebhookController {
@PostMapping("/webhook")
public ResponseEntity<Void> handleWebhook(@RequestBody String webhookEvent) {
System.out.println("Received webhook: " + webhookEvent);
return ResponseEntity.ok().build();
}
}
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("webhook")]
public class WebhookController : ControllerBase
{
[HttpPost]
public IActionResult HandleWebhook([FromBody] object webhookEvent)
{
Console.WriteLine("Received webhook: " + webhookEvent);
return Ok();
}
}
require 'json'
post '/webhook' do
request.body.rewind
webhook_event = JSON.parse(request.body.read)
puts "Received webhook: #{webhook_event['event_type']}"
status 200
end
<?php
header("Content-Type: application/json");
$webhookEvent = json_decode(file_get_contents("php://input"), true);
error_log("Received webhook: " . $webhookEvent['event_type']);
http_response_code(200);
?>
2. Verify the webhook signature
Each webhook from PayPal includes a signature. Use the /v1/notifications/verify-webhook-signature
endpoint to verify the header.
Required headers
- PAYPAL-TRANSMISSION-ID
- PAYPAL-TRANSMISSION-TIME
- PAYPAL-CERT-URL
- PAYPAL-AUTH-ALGO
- PAYPAL-TRANSMISSION-SIG
Verification examples
- cURL
- Node.js
- Python
- TypeScript
- Java
- .NET (C#)
- Ruby
- PHP
curl -X POST https://api.paypal.com/v1/notifications/verify-webhook-signature \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-d '{
"transmission_id": "<PAYPAL-TRANSMISSION-ID>",
"transmission_time": "<PAYPAL-TRANSMISSION-TIME>",
"cert_url": "<PAYPAL-CERT-URL>",
"auth_algo": "<PAYPAL-AUTH-ALGO>",
"transmission_sig": "<PAYPAL-TRANSMISSION-SIG>",
"webhook_id": "<WEBHOOK_ID>",
"webhook_event": <WEBHOOK_EVENT>
}'
The following request is in Node.js with Axios.
const verifyResponse = await axios.post(
"https://api.paypal.com/v1/notifications/verify-webhook-signature",
{
transmission_id: req.headers["paypal-transmission-id"],
transmission_time: req.headers["paypal-transmission-time"],
cert_url: req.headers["paypal-cert-url"],
auth_algo: req.headers["paypal-auth-algo"],
transmission_sig: req.headers["paypal-transmission-sig"],
webhook_id: WEBHOOK_ID,
webhook_event: req.body
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
if (verifyResponse.data.verification_status !== 'SUCCESS') {
return res.sendStatus(400);
}
import requests
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
data = {
"transmission_id": transmission_id,
"transmission_time": transmission_time,
"cert_url": cert_url,
"auth_algo": auth_algo,
"transmission_sig": transmission_sig,
"webhook_id": webhook_id,
"webhook_event": webhook_event
}
response = requests.post(
"https://api.paypal.com/v1/notifications/verify-webhook-signature",
json=data,
headers=headers
)
if response.json().get("verification_status") != "SUCCESS":
print("Verification failed")
import axios from 'axios';
const verifyResponse = await axios.post(
"https://api.paypal.com/v1/notifications/verify-webhook-signature",
{
transmission_id: req.headers["paypal-transmission-id"],
transmission_time: req.headers["paypal-transmission-time"],
cert_url: req.headers["paypal-cert-url"],
auth_algo: req.headers["paypal-auth-algo"],
transmission_sig: req.headers["paypal-transmission-sig"],
webhook_id: WEBHOOK_ID,
webhook_event: req.body
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
if (verifyResponse.data.verification_status !== 'SUCCESS') {
console.error("Verification failed");
}
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
import java.util.Map;
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + accessToken);
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = Map.of(
"transmission_id", transmissionId,
"transmission_time", transmissionTime,
"cert_url", certUrl,
"auth_algo", authAlgo,
"transmission_sig", transmissionSig,
"webhook_id", webhookId,
"webhook_event", webhookEvent
);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(
"https://api.paypal.com/v1/notifications/verify-webhook-signature",
request,
Map.class
);
if (!"SUCCESS".equals(response.getBody().get("verification_status"))) {
System.out.println("Verification failed");
}
using System.Net.Http;
using System.Text;
using Newtonsoft.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}");
var payload = new
{
transmission_id = transmissionId,
transmission_time = transmissionTime,
cert_url = certUrl,
auth_algo = authAlgo,
transmission_sig = transmissionSig,
webhook_id = webhookId,
webhook_event = webhookEvent
};
var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.paypal.com/v1/notifications/verify-webhook-signature", content);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine("Verification failed");
}
require 'net/http'
require 'json'
uri = URI("https://api.paypal.com/v1/notifications/verify-webhook-signature")
headers = {
"Authorization" => "Bearer #{access_token}",
"Content-Type" => "application/json"
}
body = {
transmission_id: transmission_id,
transmission_time: transmission_time,
cert_url: cert_url,
auth_algo: auth_algo,
transmission_sig: transmission_sig,
webhook_id: webhook_id,
webhook_event: webhook_event
}
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path, headers)
request.body = body.to_json
response = http.request(request)
if JSON.parse(response.body)["verification_status"] != "SUCCESS"
puts "Verification failed"
end
<?php
$ch = curl_init();
$data = [
"transmission_id" => $transmission_id,
"transmission_time" => $transmission_time,
"cert_url" => $cert_url,
"auth_algo" => $auth_algo,
"transmission_sig" => $transmission_sig,
"webhook_id" => $webhook_id,
"webhook_event" => $webhook_event
];
curl_setopt($ch, CURLOPT_URL, "https://api.paypal.com/v1/notifications/verify-webhook-signature");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $access_token",
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$responseData = json_decode($response, true);
if ($responseData["verification_status"] !== "SUCCESS") {
error_log("Verification failed");
}
?>
3. Register your webhook
Go to the Developer Dashboard → Select your app → Webhooks → Add URL.
Select events such as:
- CHECKOUT.ORDER.APPROVED
- PAYMENT.CAPTURE.COMPLETED
4. Test using PayPal’s webhook simulator
- Go to the webhook simulator.
- Select your app and endpoint.
- Choose an event.
- Send a test webhook.
Your server should log or respond to the event.
Best practices
- Always verify signatures for security.
- Respond with HTTP 200 if the event is received.
- Log all webhook attempts for auditing.
- Make the webhook idempotent and handle duplicate deliveries gracefully.
- Use retry logic. PayPal will retry for up to 3 days if your server fails.