Published on

Consuming Recharge Payments Webhooks through AWS API Gateway

Authors
  • avatar
    Name
    Amit Bisht
    Twitter

Problem Statement

On Recharge Payments, in the event of a card payment failure, we utilize a third-party dunning service. This service suggests a future date with a high probability of successful charging, and we subsequently update Recharge to automatically retry the transaction on that specified date.

Proposed Infrastructure

This use case was compelling us to adopt a serverless solution. Consequently, we proceeded with a robust combination of API Gateway + Lambda.

Amazon API Gateway is a fully managed service offered by Amazon Web Services (AWS) that allows developers to create, publish, maintain, monitor, and secure APIs at any scale. It acts as a gateway for managing and deploying APIs, making it easier to connect applications to back-end services.

AWS Lambda is a serverless computing service provided by Amazon Web Services (AWS) that allows developers to run code without the need to provision or manage servers. It is designed to execute functions in response to events and automatically scales based on demand.

Why ????

  • We don't need a long-running server.
  • Our business logic also wasn't long-running; it took less than 3 seconds.
  • We aimed to deploy and forget.
  • Low Cost.
  • There was basically no measurable computation; only API calls were made, with JSON being the only data sent. (FYI, API Gateway has a limit on file size if you decide to create a file upload API.)

Apache Velocity template

API Gateway and Lambda are independent, decoupled services that can be used in conjunction with multiple other AWS services. Both have individual ways of sending and receiving data. Thus, some sort of data transformation is required, which can be done in different ways at the gateway.

  • Lambda Proxy

    While creating a REST method on API Gateway and setting the target to a lambda (we will discuss this in detail below), you can choose to integrate it as a lambda proxy integration. This transforms the data received by the Gateway, including headers and the body, into a single event object, which is then passed on to the lambda for consumption.

    However, this didn't work for us, as we also needed to implement webhook source verification, and generating the HMAC256 of the payload required a very specific kind of stringified JSON in the body, which we were not obtaining through the proxy.

  • Mapping template

    By using Apache Velocity Template Language we can fine tune the data that is forwarded by Api Gateway. Read More

mapping-template
{
  "body": $input.json('$'),
  "rawbody": "$util.escapeJavaScript($input.body)",
  "headers": {
    #foreach($header in $input.params().header.keySet())
    "$header": "$util.escapeJavaScript($input.params().header.get($header))" #if($foreach.hasNext),#end

    #end
  },
  "method": "$context.httpMethod",
  "params": {
    #foreach($param in $input.params().path.keySet())
    "$param": "$util.escapeJavaScript($input.params().path.get($param))" #if($foreach.hasNext),#end

    #end
  },
  "query": {
    #foreach($queryParam in $input.params().querystring.keySet())
    "$queryParam": "$util.escapeJavaScript($input.params().querystring.get($queryParam))" #if($foreach.hasNext),#end

    #end
  }  
}

Let me explain

"body" : $input.json('$'),

We receive the payload body.

"rawbody": "$util.escapeJavaScript($input.body)",

We obtain the body in its raw form, as a JSON string, preserved exactly as the API sends it to us. This is essential for generating the webhook verification hash.

"headers": {
  #foreach($header in $input.params().header.keySet())
  "$header": "$util.escapeJavaScript($input.params().header.get($header))" #if($foreach.hasNext),#end

  #end
 },

A straightforward for loop is employed to iterate through all headers and create a header object.

"query": {
  #foreach($queryParam in $input.params().querystring.keySet())
  "$queryParam": "$util.escapeJavaScript($input.params().querystring.get($queryParam))" #if($foreach.hasNext),#end

  #end
 }  

A basic for loop is utilized to iterate through all query parameters and generate a query object.

This is a generalized template that can be applied to any lambda function and can be further customized if specific adjustments are needed.

Get Tokens from recharge

Go to their console and generate the API token. We will need the API token and the client secret (auto-generated when you create the token; simply go to the token details page and copy it).

Follow this doc and don't forget to choose the correct scope if you need to use the Recharge API further; they mention it in the API documentation.

We ended up using two additional APIs mentioned below, so choose the scope accordingly.

Create AWS Lambda

  • Login to the AWS Console and go to the Lambda section to create a new function. New Lambda
  • Create a Lambda with basic configuration. We used Python. python Lambda
  • Add handler Code.

lambda_function.py
import json
import urllib3
import os
import hashlib

http = urllib3.PoolManager()
headers = {'Content-Type': 'application/json', 'accept': 'application/json'}

RECHARGE_TOKEN = os.getenv('RECHARGE_API_TOKEN')
RECHARGE_CLIENT_SECRET = os.getenv('RECHARGE_CLIENT_SECRET')
    
    
def isWebhookValid(request_body, webhook_hmac):
    client_secret = RECHARGE_CLIENT_SECRET
    calculated_hmac = hashlib.sha256()
    calculated_hmac.update(client_secret.encode("UTF-8"))
    calculated_hmac.update(request_body.encode("UTF-8"))
    calculated_hmac = calculated_hmac.hexdigest()
    
    if calculated_hmac == webhook_hmac:
        print("Webhook validated successfully!")
        return True
    else:
        print("Oops! There may be some third party interference going on.")
        return False


def get_data(url, headers=None):
    r = http.request('GET', url, headers=headers)
    return json.loads(r.data)


def post_data(url, obj, headers=None):
    r = http.request('POST', url, headers=headers, body=json.dumps(obj))
    return json.loads(r.data)
   
def lambda_handler(event, context):
    try:
        print(event)
        isValid = isWebhookValid(event["rawbody"], event["headers"]['X-Recharge-Hmac-Sha256'])
        
        if not isValid:
            return
        
        webhookBody = event["body"]
        print(webhookBody)
        
        # Add your business logic now. You can use get_data and post_data to perform API calls, 
        # or attach a layer to this Lambda containing your favorite package and use that.
        
        
        return {
            'statusCode': 200,
        }
    except Exception as e:
      print(e)
  • Add keys to the Lambda environment.

    Go to Lambda's configuration -> Environment variables and add RECHARGE_API_TOKEN, RECHARGE_CLIENT_SECRET.

Create API Gateway

  • Go to the API Gateway section and create a new REST API. api gateway type
  • Create a new API. api gateway config
  • Create a new resource in that API. api gateway resource
  • Add the resource name. api gateway resource config
  • Create an HTTP method on the resource. api gateway method
  • Create a POST type method and select the target as the Lambda function we created earlier. api gateway method config
  • Select the method and edit the Integration Request tab to perform manipulation. api gateway integration request
  • Change the "Request body pass through" setting. api gateway pass through
  • Scroll all the way down and add our mapping template. api gateway mapping
  • Now, it's finally time to deploy the API. api gateway deploy button
api gateway deploy dialog

Register the webhook on the Recharge platform.

So Recharge does not provide a UI for it. They expose public endpoints to create, list, and delete them. Find them here

FYI, use the same API token that you added to Lambda.

And voilà, we are done.
Thanks for reading!