Skip to content

Instantly share code, notes, and snippets.

@host-anshu
Last active November 22, 2022 15:56
Show Gist options
  • Save host-anshu/3b2887131057c99e559d6d46d3ea84b9 to your computer and use it in GitHub Desktop.
Save host-anshu/3b2887131057c99e559d6d46d3ea84b9 to your computer and use it in GitHub Desktop.
Verify PayPal Webhook Signature using the REST APIs and the PayPal-Python-SDK
"""
This snippet could be used to verify PayPal webhooks
PayPal docs isn't very clear about API verification mechanism. And am afraid neither
is the SDK.
BASIC CONCEPT: Your webhook_event in the body of verification request be in the same
order you have received from PayPal or else your verification fails.
EXTRA TIP IF USING DRF: Don't use request.DATA as it mutates the order of the request
and you can't retrieve it anymore using request.body (which is also request.stream.body)
Credit: https://github.com/paypal/PayPal-Python-SDK/issues/196
The above link gave me idea about the SDK method, which I implemented as the fallback
method. Particularly because it's archived in favour of new SDK, that doesn't yet have
webhook support(another reason why I write this gist :-D ). This also helped me knowing
the basic concept I mentioned above.
"""
import json
import logging
import os
# paypalrestsdk==1.13.1
import paypalrestsdk as paypal
from collections import OrderedDict
from functools import wraps
from paypalrestsdk import WebhookEvent
# I have used Django and DRF here
# Django==1.6.10
# djangorestframework==2.4.3
from django.conf import settings
from rest_framework.exceptions import ParseError
# Note: I've more setup in settings that make it work. It might not work directly.
logger = logging.getLogger(__name__)
paypal_api = paypal.configure({
'client_id': os.getenv("PAYPAL_CLIENT_ID"),
'client_secret': os.getenv("PAYPAL_CLIENT_SECRET"),
})
class PayPalAPIError(Exception):
"""Error when PayPal API fails"""
class BadRequest(ParseError):
"""Return HTTP 400 response when raised"""
class PaypalSignatureVerificationError(BadRequest):
"""Handles paypal signature verification errors."""
def handle_api_error(func):
@wraps(func)
def wrapper(*args, **kwargs):
response = func(*args, **kwargs)
error = response.get("error", None)
if error:
raise PayPalAPIError(error)
return response
return wrapper
@handle_api_error
def verify_webhook_signature(data):
return paypal_api.post(
"/v1/notifications/verify-webhook-signature",
params=data,
headers={"Content-Type": "application/json"}
)
def verify_webhook_signature_using_sdk(*args):
return WebhookEvent.verify(*args)
def verify_event(request):
auth_algo = request.META.get("HTTP_PAYPAL_AUTH_ALGO")
cert_url = request.META.get("HTTP_PAYPAL_CERT_URL")
transmission_id = request.META.get("HTTP_PAYPAL_TRANSMISSION_ID")
transmission_sig = request.META.get("HTTP_PAYPAL_TRANSMISSION_SIG")
transmission_time = request.META.get("HTTP_PAYPAL_TRANSMISSION_TIME")
webhook_id = settings.PAYPAL_WEBHOOK_ID
# The link I gave credit above discusses about decoding it. I didn't use it.
# But am not sure when you need it. Please educate me if you come across a usage
# that needs it.
ordered_payload = json.loads(request.body, object_pairs_hook=OrderedDict)
data = dict(
auth_algo=auth_algo, cert_url=cert_url, transmission_id=transmission_id,
transmission_sig=transmission_sig, transmission_time=transmission_time,
webhook_id=webhook_id, webhook_event=ordered_payload
)
try:
signature_verification = verify_webhook_signature(data)
if signature_verification.get("verification_status") == "SUCCESS":
return True
except Exception as err:
logger.warning("Error verifying event using API: %s", err)
try:
# Fallback using SDK. Kept for those who are still using it.
signature_verification = verify_webhook_signature_using_sdk(
transmission_id, transmission_time, webhook_id, request.body, cert_url,
transmission_sig, auth_algo)
if signature_verification:
return signature_verification
except Exception as alter_err:
logger.warning("Error verifying event using SDK: %s", alter_err)
raise PaypalSignatureVerificationError("Paypal Webhook Signature Verification failed")
@host-anshu
Copy link
Author

I must also note that it has not been extensively tested.

@FreelanceDev217
Copy link

Not work.

Error verifying event using API: 'Response' object has no attribute 'get'
Error verifying event using SDK: 'bytes' object has no attribute 'encode'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment