Flask database not updated after stripe webhook completed - flask

this is my first time writing a flask project and I find it easy to understand. I am working on a subscription base project and I stumbled a problem that I cannot get my head around.
I really appreciate it if anyone can help. Thanks so much. Looking forward to your response.
#payments.route('/webhook', methods=['POST'])
def webhook_received():
# You can use webhooks to receive information about asynchronous payment events.
# For more about our webhook events check out https://stripe.com/docs/webhooks.
webhook_secret = os.getenv('STRIPE_WEBHOOK_SECRET')
request_data = json.loads(request.data)
if webhook_secret:
# Retrieve the event by verifying the signature using the raw body and secret if webhook signing is configured.
signature = request.headers.get('stripe-signature')
try:
event = stripe.Webhook.construct_event(
payload=request.data, sig_header=signature, secret=webhook_secret)
data = event['data']
except Exception as e:
return e
# Get the type of webhook event sent - used to check the status of PaymentIntents.
event_type = event['type']
else:
data = request_data['data']
event_type = request_data['type']
data_object = data['object']
print('event ' + event_type)
if event_type == 'checkout.session.completed':
# Handle the checkout.session.completed event
session = data['data']['object']
#find and update the user's subscription
user = UserMixin.query.filter_by(username=current_user.username).first()
user.subscriptionStatus = True
user.stripe_customer_id = session['customer']
user.subscriptionItem = session['subscription_items'][0]['price']['lookup_key']
user.price = session['subscription_items'][0]['plan']['amount']
if user.subscriptionItem == 'Starter':
user.num_words = 20000
elif user.subscriptionItem == 'Professional':
user.num_words = 50000
db.session.commit()
print('🔔 Payment succeeded!')
return jsonify({'status': 'success'})

Related

Not getting Django paypal IPN handshake

I am trying to implement Django- PayPal in my project , now it has been 3 days , I am still stuck on this , I don't understand how we perform IPN handshake, I am receiving a signal from PayPal after payment but what is the next step after this , really frustrated there are no clear docs about this process help needed , thanks in advance
Signals.py
def show_me_the_money(sender, **kwargs):
ipn_obj = sender
if ipn_obj.payment_status == ST_PP_COMPLETED:
# WARNING !
# Check that the receiver email is the same we previously
# set on the `business` field. (The user could tamper with
# that fields on the payment form before it goes to PayPal)
if ipn_obj.receiver_email != settings.PAYPAL_RECEIVER_EMAIL:
# Not a valid payment
print('reciever mail is diff')
print(ipn_obj.receiver_email)
# ALSO: for the same reason, you need to check the amount
# received, `custom` etc. are all what you expect or what
# is allowed.
# Undertake some action depending upon `ipn_obj`.
if ipn_obj.custom == "premium_plan":
price = ...
else:
price = ...
if ipn_obj.mc_gross == price and ipn_obj.mc_currency == 'USD':
...
else:
pass
#...
valid_ipn_received.connect(show_me_the_money)
Urls.py
path('payment/',PaymentProcess.as_view(),name='payment-process'),
path('payment_redirect/',Payment.as_view(),name='payment-redirect'),
path('createorder/',CreateOrderView.as_view(),name='create-order'),
# Paypal IPN url ------------------
re_path(r'^paypal/', include('paypal.standard.ipn.urls')),
path('payment_done', payment_done,name='payment_done'),
I mean where should the below code reside or do I even need to process it, as I see other resources doing it , they are sending Response back to Paypal as confirming the payment https://github.com/paypal/ipn-code-samples/blob/master/python/paypal_ipn.py
verify_url = settings.VERIFY_URL_TEST
print ('content-type: text/plain')
print ()
print('SIgnal form paypal')
param_str = sys.stdin.readline().strip()
print(param_str)
params = urllib.parse.parse_qsl(param_str)
params.append(('cmd', '_notify-validate'))
print(params)
headers = {'content-type': 'application/x-www-form-urlencoded',
'user-agent': 'Python-IPN-Verification-Script'}
r = requests.post(verify_url, params=params, headers=headers, verify=True)
r.raise_for_status()
print(r)

Django Session Variables Don't Work In Stripe Webhook?

I am trying to use data saved in django session variables to run a function once the webhook has confirmed that 'checkout.session.completed' but I always get a key error. I am 100% sure the keys exist in the session variables.
Here is my webhook:
#csrf_exempt
def stripe_webhook(request):
# You can find your endpoint's secret in your webhook settings
endpoint_secret = 'secret'
payload = request.body
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
event = None
try:
event = stripe.Webhook.construct_event(
payload, sig_header, endpoint_secret
)
except ValueError as e:
# Invalid payload
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return HttpResponse(status=400)
# Handle the checkout.session.completed event
if event['type'] == 'checkout.session.completed':
session = event['data']['object']
fulfull_order(session)
return HttpResponse(status=200)
Here is my fulfill order function:
def fulfull_order(session):
generator = PlanMaker(goal=request.session['goal'], gender=request.session['gender'])
/// send email code.
This line generator = PlanMaker(goal=request.session['goal'], gender=request.session['gender'])
Always gives a key error on request.session['goal'] The key definitely exists, it just seems it is inaccessible from the webhook view.
How to solve?
You should save the information you want to the metadata field when creating the checkout.Session.
def checkout(request):
session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=[{
'price': 'price_key',
'quantity': 1,
}],
mode='payment',
success_url=request.build_absolute_uri(reverse('success_url')) + '?session_id={CHECKOUT_SESSION_ID}',
cancel_url=request.build_absolute_uri(reverse('cancel_url')),
metadata={'someKeyHere': 'your session variable data'}
)
return JsonResponse({
'session_id' : session.id,
'stripe_public_key' : settings.STRIPE_PUBLISHABLE_KEY
})
then you can access the information like session['metadata']['someKeyHere']
The webhook event is a separate request coming directly from Stripe that would not be related to any Django session and so this lack of session data would seem expected. As #Anthony suggests you can store this information in the Checkout Session metadata when you create the session. The metadata will be included in the webhook object.

Stripe with Django - Retrieve product / price from Session

I'm integrating Stripe Checkout with my django website. I have 2 products and everytime a PaymentIntent is successful, I want to fetch the Price related (the product that was bought).
I have 2 checkouts, one for each product, and a webhook to listen.
views.py - Create a purchase session (exist twice for product 1 & 2)
#csrf_exempt
def create_checkout_session_product1(request):
if request.method == 'GET':
domain_url = 'example.com'
checkout_session = stripe.checkout.Session.create(
success_url=domain_url + 'paiement_ok/',
cancel_url=domain_url + 'paiement_ko/',
payment_method_types=['card'],
line_items=[
{
"price" : "price_1...", <----- What I want to fetch
"quantity": 1,
},
],
mode='payment',
customer_email=request.user.get_username(),
)
return JsonResponse({'sessionId': checkout_session['id']})
views.py - Webhook to trigger a process after a purchase
#csrf_exempt
def webhook(request):
payload = request.body
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
event = None
try:
event = stripe.Webhook.construct_event(
payload, sig_header, endpoint_secret
)
except ValueError as e:
# Invalid payload
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return HttpResponse(status=400)
# Handle the checkout.session.completed event
if event['type'] == 'checkout.session.completed':
session = event['data']['object']
#then I'd want to do something like :
line_items = stripe.checkout.Session.list_line_items(session.id)
price = line_items.data.price.id
if price == product1:
activate_process_product1(session.customer_email)
elif price == product2:
activate_process_product2(session.customer_email)
# Passed signature verification
return HttpResponse(status=200)
So the webhook works since I get the money.
But I don't get how to fetch the product once the purchase is complete ?
If you have a Payment Intent that was created from a Checkout Session, you need to go back and retrieve the associated Checkout Session to get price/product information.
Get the ID of the Payment Intent.
Make a request to list Checkout Sessions [0] while setting payment_intent and having data.line_items expanded [1]. The call in python should look like this: sessions = stripe.checkout.Session.list(payment_intent='pi_xxx', expand=['data.line_items'])
If the call was successful and returned a non-empty list of Checkout Sessions, take the first one and check line_items for information on the price and product.
[0] https://stripe.com/docs/api/checkout/sessions/list#list_checkout_sessions-payment_intent
[1] https://stripe.com/docs/expand#lists
One way I've found to get it from the Session was to use the stripe.checkout.Session.list_line_items method.
if event['type'] == 'checkout.session.completed':
session = event['data']['object']
line_items = stripe.checkout.Session.list_line_items(session.id)
price_id = line_items.data[0]['price']['id']
got me the price id.
Source : Stripe doc

Django/Stripe: idempotent requests can only be used with the same parameters

I am using Stripe in my Django application. I have the following test case: incorrect_cvc leads to an card_error. Now when correcting the CVC and using 4242 4242 4242 4242 what I except is a successful charge. However what I get pack is the following error message:
Request req_auTSTGSGoUVNUa: Keys for idempotent requests can only be
used with the same parameters they were first used with. Try using a
key other than 'k1qjchgqjw' if you meant to execute a different
request.
I am not aware of which parameters I changed. But I think it's not the idea that the checkout process basically doesn't work anymore after an card_error. Does anyone understand which parameters I "changed" that leads to this error message?
def checkout_page(request):
"""
* Check if session and ReservedItem exist.
* Generate order_item dict for every ReservedItem entry, that belongs
to order_reference.
If request.method is 'POST':
* Check if ticket reservation is still valid.
* Create entries in models OrderItem, Order & ReservedItem.
"""
session_order_reference = request.session.get('order_reference')
if request.session.get('order_reference'):
reserved_items = ReservedItem.objects.filter(
order_reference=session_order_reference
)
if not reserved_items:
return redirect('website:index')
else:
return redirect('website:index')
taxes_dict = {}
total_gross = total_tax_amount = 0
order_items_list = []
for item in reserved_items:
event = item.ticket.event
timestamp_of_reservation = item.created
total_gross += item.subtotal
order_item = {
'ticket': item.ticket,
'ticket_name': item.ticket.name,
'quantity': item.quantity,
'subtotal': item.subtotal,
'type': OrderType.ORDER,
}
total_tax_amount += add_tax(
item=item,
taxes_dict=taxes_dict,
order_item=order_item,
)
order_items_list.append(dict(order_item))
total_net = total_gross - total_tax_amount # TODO Marc: Calculate in add_vat func?
if request.method == 'POST':
# TODO Marc: Should live in forms.py or just models?
reservation_expired_redirect = check_if_reservation_expired(
request=request,
timestamp_of_reservation=timestamp_of_reservation,
organizer=event.organizer.slug,
event=event.slug,
)
if reservation_expired_redirect:
return reservation_expired_redirect
# TODO Marc: Should live in forms.py or just models?
ticket_is_on_sale = check_if_ticket_is_on_sale(
order_items_list=order_items_list,
request=request,
organizer=event.organizer.slug,
event=event.slug,
)
if ticket_is_on_sale:
return ticket_is_on_sale
billing = BillingForm(request.POST, prefix='billing')
order = OrderForm(request.POST, prefix='order')
if order.is_valid() and billing.is_valid():
# Charge via Stripe
stripe.api_key = "ABC" # TODO Marc: Change to env
token = request.POST.get('stripeToken')
# https://stripe.com/docs/api#error_handling
paid = False
try:
# Compare with transactions > models copy.py > class ChargeManager(models.Manager):
# Use Stripe's library to make requests...
total_gross_amount_in_smallest_unit = smallest_currency_unit(total_gross, 'eur') #TODO Marc: Replace eur
charge = stripe.Charge.create(
amount=total_gross_amount_in_smallest_unit, # TODO Marc > https://stripe.com/docs/currencies#zero-decimal
application_fee=100, # TODO Marc: Which currency?
currency='eur', # TODO Marc
source=token,
stripe_account="ABC", # TODO Marc: Replace with organizer stripe account
idempotency_key=session_order_reference,
)
new_charge_obj = Charge.objects.create(
amount=charge.amount,
charge_id=charge.id,
livemode=charge.livemode,
paid=charge.paid,
refunded=charge.refunded,
currency=charge.currency,
failure_code=charge.failure_code,
failure_message=charge.failure_message,
fraud_details=charge.fraud_details,
outcome=charge.outcome,
status=charge.status,
application_fee=charge.application_fee,
captured=charge.captured,
created=charge.created,
# TODO Marc: Add refunds:
# amount_refunded=charge.amount_refunded,
# etc.
)
application_fee = stripe.ApplicationFee.retrieve(charge.application_fee)
Fee.objects.create(
fee_id=application_fee.id,
livemode=application_fee.livemode,
currency=application_fee.currency,
amount=application_fee.amount,
charge=new_charge_obj,
# TODO Marc: Add refunds
)
paid = new_charge_obj.paid
except stripe.error.CardError as e:
# Since it's a decline, stripe.error.CardError will be caught
body = e.json_body
err = body.get('error', {})
messages.add_message(
request,
messages.ERROR,
err.get('message')
)
# return redirect(
# 'orders:order-list',
# order_reference=new_order.order_reference,
# access_key=new_order.access_key,
# )
# print("Type is: %s") % err.get('type')
# print("Code is: %s") % err.get('code')
# # param is '' in this case
# print("Param is: %s") % err.get('param')
# print("Message is: %s") % err.get('message')
except stripe.error.RateLimitError as e:
# Too many requests made to the API too quickly
pass
except stripe.error.InvalidRequestError as e:
# Invalid parameters were supplied to Stripe's API
pass
except stripe.error.AuthenticationError as e:
# Authentication with Stripe's API failed
# (maybe you changed API keys recently)
pass
except stripe.error.APIConnectionError as e:
# Network communication with Stripe failed
pass
except stripe.error.StripeError as e:
# Display a very generic error to the user, and maybe send
# yourself an email
pass
except Exception as e:
# Something else happened, completely unrelated to Stripe
pass
if paid:
# Create new attendee
i = 1
attendee_list = []
for item in reserved_items:
for _ in range(item.quantity): # noqa
new_attendee_dict = {
'event': item.ticket.event,
'ticket': item.ticket,
'ticket_name': item.ticket.name,
'ticket_reference': session_order_reference + "-" + str(i),
'ticket_code': get_random_string(length=10),
}
i += 1
attendee_list.append(dict(new_attendee_dict))
# Create new order
new_order_dict = {
'total_gross': total_gross,
'total_tax': total_tax_amount,
'total_net': total_net,
'total_gross_converted': total_gross, # TODO Marc
'event': event,
'order_reference': session_order_reference,
'status': OrderStatus.PENDING,
'access_key': get_random_string(length=10),
}
new_order = order.save(commit=False)
[setattr(new_order, k, v) for k, v in new_order_dict.items()]
new_order.save()
# Create order items
for item in order_items_list:
OrderItem.objects.create(order=new_order, **item)
# Create attendees
for item in attendee_list:
Attendee.objects.create(order=new_order, **item)
# Create billing profile
billing_profile = billing.save(commit=False)
billing_profile.order = new_order
billing_profile.save()
# Delete order_reference session
del request.session['order_reference']
return redirect(
'orders:order-list',
order_reference=new_order.order_reference,
access_key=new_order.access_key,
)
else:
billing = BillingForm(prefix='billing')
order = OrderForm(prefix='order')
context = {
'reserved_items': reserved_items,
'taxes': taxes_dict,
'total_net': total_net,
'total_gross': total_gross,
'currency': event.currency,
'order': order,
'billing': billing,
}
return render(request, 'checkout/checkout.html', context)
The problem is not with anything that you've changed, but rather what you haven't changed :)
On this line you are passing an idempotency_key:
charge = stripe.Charge.create(
...
idempotency_key=session_order_reference,
)
As described in the Stripe docs, you can pass an idempotency key with a request, which allows you to make the same request again in the future, using the same key, and you will get the same result as the first request. This is useful in case you didn't recieve the first response because of a network issue.
In this case, you have changed the CVC, which creates a new token variable. This means that your request is not identical to the previous request that used the same idempotency key. That doesn't make sense as you can only use the same idempotency key with identical requests, so you get this error from Stripe.
To resolve this, you should retry the charge creation using a freshly generated idempotency key. Generally, the key should be generated on each unique request that your application creates.
Had a similar issue where I was passing an indempotency_key and clients were not able to pay after their card was declined because the data that was sent was unique to the charge but not to the card. If for example their CVC was incorrect the subsequent charge will get created with the exact same idempotency key because data pertinent to the actually card was not taken into account.
The fix for this is to make sure your key is unique to the charge AND the card in this case including the card token can fix this.
Some other things to think about are things like partial payments, refunds, same/different ip, other metadata.
Stripe handles this case
when you are submitting wrong cvv
I tested with stripe test credit cards
https://stripe.com/docs/testing#cards
use that one which fails with cvv code than use valid card.

Automatically processing the Amazon SES notifications for bounce and complaint notifications

We are using AWS SES for sending mails. Amazon SES sends bounce and complaint notifications through emails or AWS SNS. We would like to automatically process the bounce and complaint notifications (coming from email or AWS SNS) to extract the email ids, so that these emails can be removed from the original list.
One way to automate is to send these notifications to a topic in AWS SNS, then subscribe to the topic using AWS SQS and finally read the messages in the AWS SQS. SNS supports subscription over the following protocols - HTTP/HTTPS/EMail/EMail(JSON)/SMS/SQS. This is feasible, but I find it too cumbersome for a simple task of automatically processing the bounce and complaint notifications.
Is there any elegant way of tacking this problem?
I have found a blog entry from Amazon with the code in C#. Is there a better solution?
I find that directly subscribing to SNS using an HTTP endpoint is the most straightforward approach. You literally have to write only a few lines of code. Here's my django example:
def process(request):
json = request.raw_post_data
js = simplejson.loads(json)
info = simplejson.loads(js["Message"])
type = info["notificationType"] # "Complaint" or "Bounce"
email = info["mail"]["destination"][0]
# do whatever you want with the email
I think the way you describe IS probably the most elegant way. You already have very appropriate services in SNS and SQS that have associated SDK's in most major languages to allow you to do what you need easily. The hardest part is writing the code to update/remove the records in your mailing lists.
Through trial an error I have come up with this one - it is for Django but does a decent job for me.
First the models, then the request handler...
class ComplaintType:
ABUSE = 'abuse'
AUTH_FAILURE = 'auth-failure'
FRAUD = 'fraud'
NOT_SPAM = 'not-spam'
OTHER = 'other'
VIRUS = 'virus'
COMPLAINT_FEEDBACK_TYPE_CHOICES = [
[ComplaintType.ABUSE, _('Unsolicited email or some other kind of email abuse')],
[ComplaintType.AUTH_FAILURE, _('Unsolicited email or some other kind of email abuse')],
[ComplaintType.FRAUD, _('Some kind of fraud or phishing activity')],
[ComplaintType.NOT_SPAM, _('Entity providing the report does not consider the message to be spam')],
[ComplaintType.OTHER, _('Feedback does not fit into any other registered type')],
[ComplaintType.VIRUS, _('A virus was found in the originating message')]
]
class SES_Complaint(models.Model):
subject = models.CharField(max_length=255)
message = models.TextField()
email_address = models.EmailField(db_index=True)
user_agent = models.CharField(max_length=255)
complaint_feedback_type = models.CharField(max_length=255, choices=COMPLAINT_FEEDBACK_TYPE_CHOICES)
arrival_date = models.DateTimeField()
timestamp = models.DateTimeField()
feedback_id = models.CharField(max_length=255)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'SES Complaint'
verbose_name_plural = 'SES Complaints'
def get_reason(self):
return self.get_complaint_feedback_type_display()
class BounceType:
UNDETERMINED = 'Undetermined'
PERMANENT = 'Permanent'
TRANSIENT = 'Transient'
class BounceSubType:
UNDETERMINED = 'Undetermined'
GENERAL = 'General'
NO_EMAIL = 'NoEmail'
SUPPRESSED = 'Suppressed'
MAILBOX_FULL = 'MailboxFull'
MESSAGE_TOO_LARGE = 'MessageToolarge'
CONTENT_REJECTED = 'ContentRejected'
ATTACHMENT_REJECTED = 'AttachmentRejected'
BOUNCE_TYPE_CHOICES = [
[BounceType.UNDETERMINED, _('Unable to determine a specific bounce reason')],
[BounceType.PERMANENT, _('Unable to successfully send')],
[BounceType.TRANSIENT, _('All retry attempts have been exhausted')],
]
BOUNCE_SUB_TYPE_CHOICES = [
[BounceSubType.UNDETERMINED, _('Unable to determine a specific bounce reason')],
[BounceSubType.GENERAL, _('General bounce. You may be able to successfully retry sending to that recipient in the future.')],
[BounceSubType.NO_EMAIL, _('Permanent hard bounce. The target email address does not exist.')],
[BounceSubType.SUPPRESSED, _('Address has a recent history of bouncing as invalid.')],
[BounceSubType.MAILBOX_FULL, _('Mailbox full')],
[BounceSubType.MESSAGE_TOO_LARGE, _('Message too large')],
[BounceSubType.CONTENT_REJECTED, _('Content rejected')],
[BounceSubType.ATTACHMENT_REJECTED, _('Attachment rejected')]
]
class SES_Bounce(models.Model):
subject = models.CharField(max_length=255)
message = models.TextField()
bounce_type = models.CharField(max_length=255, choices=BOUNCE_TYPE_CHOICES)
bounce_sub_type = models.CharField(max_length=255, choices=BOUNCE_SUB_TYPE_CHOICES)
timestamp = models.DateTimeField()
feedback_id = models.CharField(max_length=255)
status = models.CharField(max_length=255)
action = models.CharField(max_length=255)
diagnostic_code = models.CharField(max_length=255)
email_address = models.EmailField(db_index=True)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True, db_index=True)
class Meta:
verbose_name = 'SES Bounce'
verbose_name_plural = 'SES Bounces'
def get_reason(self):
return '%s - %s' % (self.get_bounce_type_display(), self.get_bounce_sub_type_display())
And here is the request handler:
#csrf_exempt
def aws_sns(request):
logger.debug('Incoming SNS')
if request.method == 'POST':
logger.debug('Incoming SNS is POST')
sns_message_type = request.META.get('HTTP_X_AMZ_SNS_MESSAGE_TYPE', None)
if sns_message_type is not None:
logger.debug('Incoming SNS - %s', sns_message_type)
json_body = request.body
json_body = json_body.replace('\n', '')
js = loads(json_body)
if sns_message_type == "SubscriptionConfirmation":
subscribe_url = js["SubscribeURL"]
logger.debug('Incoming subscription - %s', subscribe_url)
urllib.urlopen(subscribe_url)
elif sns_message_type == "Notification":
message = js.get("Message", None)
message = message.replace('\n', '')
message = loads(message)
notification_type = message.get("notificationType", None)
if notification_type == 'AmazonSnsSubscriptionSucceeded':
logger.debug('Subscription succeeded')
elif notification_type == 'Bounce':
logger.debug('Incoming bounce')
bounce = message['bounce']
bounce_type = bounce['bounceType']
bounce_sub_type = bounce['bounceSubType']
timestamp = bounce['timestamp']
feedback_id = bounce['feedbackId']
bounce_recipients = bounce['bouncedRecipients']
for recipient in bounce_recipients:
status = recipient.get('status')
action = recipient.get('action')
#diagnostic_code = recipient['diagnosticCode']
email_address = recipient['emailAddress']
SES_Bounce.objects.filter(email_address=email_address).delete()
SES_Bounce.objects.create(
message=message,
bounce_type=bounce_type,
bounce_sub_type=bounce_sub_type,
timestamp=timestamp,
feedback_id=feedback_id,
status=status,
action=action,
#diagnostic_code=diagnostic_code,
email_address=email_address
)
elif notification_type == 'Complaint':
logger.debug('Incoming complaint')
complaint = message['complaint']
user_agent = complaint.get('userAgent')
complaint_feedback_type = complaint.get('complaintFeedbackType')
arrival_date = complaint.get('arrivalDate')
timestamp = complaint['timestamp']
feedback_id = complaint['feedbackId']
recipients = complaint['complainedRecipients']
for recipient in recipients:
email_address = recipient['emailAddress']
SES_Complaint.objects.filter(email_address=email_address).delete()
SES_Complaint.objects.create(
#subject=subject,
message=message,
email_address=email_address,
user_agent=user_agent,
complaint_feedback_type=complaint_feedback_type,
arrival_date=arrival_date,
timestamp=timestamp,
feedback_id=feedback_id
)
else:
logger.exception('Incoming Notification SNS is not supported: %s', notification_type)
return HttpResponse()
else:
logger.exception('Incoming SNS did not have the right header')
for key, value in request.META.items():
logger.debug('Key: %s - %s', key, value)
else:
logger.exception('Incoming SNS was not a POST')
return HttpResponseBadRequest()
Recently, I was able to get this working using an HTTP Endpoint via SNS. I use python/django to consume the notification. You have to process the subscription message first before you consume the notifications; you can read about subscriptions in the SNS documentation.
I think if you have a smaller application that doesn't send to many emails; http endpoint should work fine. This code requires that you have a notification model created.
#process an amazon sns http endpoint notification for amazon ses bounces and complaints
#csrf_exempt
def process_ses_notification(request):
if request.POST:
json_body = request.body
#remove this control character(throws an error) thats present inside the test subscription confirmation
js = loads(json_body.replace('\n', ''))
if js["Type"] == "SubscriptionConfirmation":
subscribe_url = js["SubscribeURL"]
urllib.urlopen(subscribe_url)
return HttpResponse(status=200)
elif js["Type"] == "Notification":
#process message from amazon sns
arg_info = loads(js["Message"]) # may need to use loads(js["Message"]) after testing with amazon
arg_notification_type = arg_info["notificationType"]
if arg_notification_type == 'Bounce':
#required bounce object fields
arg_emails=arg_info["bounce"]["bouncedRecipients"]
arg_notification_subtype=arg_info["bounce"]["bounceType"]
arg_feedback_id=arg_info["bounce"]["feedbackId"]
arg_date_recorded=arg_info["bounce"]["timestamp"]
elif arg_notification_type == 'Complaint':
#required complaint object fields
arg_emails=arg_info["complaint"]["complainedRecipients"]
arg_feedback_id=arg_info["complaint"]["feedbackId"]
arg_date_recorded=arg_info["complaint"]["timestamp"]
#check if feedback type is inside optional field name
if "complaintFeedbackType" in arg_info["complaint"]:
arg_notification_subtype=arg_info["complaint"]["complaintFeedbackType"]
else:
arg_notification_subtype=""
else:
HttpResponse(status=400)
#save notifications for multiple emails
for arg_email in arg_emails:
notification = SES_Notification(info=json_body, notification_type=arg_notification_type,
email=arg_email["emailAddress"], notification_subtype=arg_notification_subtype,
date_recorded=arg_date_recorded, feedback_id=arg_feedback_id)
notification.save()
return HttpResponse(status=200)
return HttpResponse(status=400)
all the answers above are great, but just a small and important addition:
you first need to verify that the request is from amazon sns: (as described at Verifying the Signatures of Amazon SNS Messages)
for the python code that validate the signature - a good example is here