bunq API: Callback Integration with Amazon EventBridge

December 10th, 2023 1117 Words

The weekend was nice; I had some fun using the bunq API and configure webhooks for account activities and explained how to use Amazon EventBridge Pipes to process and transform messages from SQS. Now, it’s time to combine both and get bunq events to Amazon EventBridge!

Architecture Overview

As for most flexible integrations, this only requires to configuration of already existing building blocks; most information is already distributed across multiple posts on this site. This guide will combine the relevant steps and information:

EventBridge Pipe and Bus

Based on the existing code for creating an Amazon EventBridge Pipe using SQS as a data source and an EventBridge Bus we will get started.

import { App, Stack } from 'aws-cdk-lib';

const app = new App()
const stack = new Stack(app, 'example')

// Configure EventBridge Bus
const bus = new EventBus(stack, 'bus')

// Configure SQS Queue
const queue = new Queue(stack, 'queue')

// Configure IAM Role for EventBridge Pipe
const pipeRolePolicyQueue = new PolicyDocument({
  statements: [
    new PolicyStatement({
      effect: Effect.ALLOW,
      resources: [ queue.queueArn ],
      actions: [ 'sqs:ReceiveMessage', 'sqs:DeleteMessage', 'sqs:GetQueueAttributes' ],
    }),
  ],
})

const pipeRolePolicyBus = new PolicyDocument({
  statements: [
    new PolicyStatement({
      effect: Effect.ALLOW,
      resources: [ bus.eventBusArn ],
      actions: [ 'events:PutEvents' ],
    }),
  ],
})

const pipeRole = new Role(stack, 'role', {
  assumedBy: new ServicePrincipal('pipes.amazonaws.com'),
  inlinePolicies: {
    queue: pipeRolePolicyQueue,
    bus: pipeRolePolicyBus,
  },
})

// Configure EventBridge Pipe to read from SQS and write to EventBridge
new CfnPipe(stack, 'pipe', {
  roleArn: pipeRole.roleArn,
  source: queue.queueArn,
  sourceParameters: {
    sqsQueueParameters: {
      batchSize: 1,
      maximumBatchingWindowInSeconds: 120,
    },
  },
  target: bus.eventBusArn,
  targetParameters: {
    eventBridgeEventBusParameters: {
      detailType: 'detail-type',
      source: `your.incoming.daa`,
    },
    inputTemplate: `{
      "message": "<$.body.message>"
    }`,
  },
})

This is a perfect baseline; to evaluate the infrastructure, you can use the AWS CLI and send a message to the Amazon SQS queue. It will be processed by the EventBridge Pipe and end up as an event on the Amazon EventBridge Bus afterwards.

$ > aws sqs send-message \
    --queue-url https://sqs.eu-central-1.amazonaws.com/12345EXAMPLE/YourQueue \
    --message-body "{\"message\": \"lorem ipsum\"}"

Nice. But how do you integrate this with a banking service? Luckily, bunq supports Callbacks for banking accounts! bunq Callbacks are like common webhooks: a plain HTTPS request to an endpoint. So we need to wrap SQS with a suitable API …

API Gateway Service Integration

One great feature of Amazon API Gateway (and other AWS services as well) is the Service Integration of other AWS services. I already explained how to integrate SQS or EventBridge in general with API Gateway:

Based on these guides, the code example above can be extended with an Amazon API Gateway and a simple route to accept incoming POST Requests and submit the payload as a message to SQS for further processing.

// Create Amazon API Gateway
const api = new RestApi(stack, 'api', {
  deployOptions: {
    tracingEnabled: true,
  },
})

Of course, an IAM Role is required to allow API Gateway to submit messages to SQS for incoming requests by the bunq Callback.

// Create IAM Policy to allow access to SQS
integrationRolePolicyQueue = new PolicyDocument({
  statements: [
    new PolicyStatement({
      effect: Effect.ALLOW,
      actions: [ 'sqs:SendMessage' ],
      resources: [ queue.queueArn ],
    }),
  ],
})

// Create IAM Role for API Gateway to assume
const integrationRole = new Role(stack, 'role', {
  assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
  inlinePolicies: {
    queue: integrationRolePolicyQueue,
  },
})

Next, the tricky part about Service Integrations using Amazon API Gateway comes to play: Mapping Templates using the Velocity Template Language by Apache. It hurts every single time, but it gets the job done. Using this, you can configure a no-code response handler for incoming requests:

const integration = new AwsIntegration({
  service: 'sqs',
  path: `${Stack.of(stack).account}/${queue.queueName}`,
  integrationHttpMethod: 'POST',
  options: {
    credentialsRole: integrationRole,
    passthroughBehavior: PassthroughBehavior.NEVER,
    requestParameters: {
      'integration.request.header.Content-Type': `'application/x-www-form-urlencoded'`,
    },
    requestTemplates: {
      'application/json': `Action=SendMessage&MessageBody=$util.urlEncode("$input.body")`,
    },
    integrationResponses: [
      {
        statusCode: '200',
        responseTemplates: {
          'application/json': `{"done": true}`,
        },
      },
    ],
  },
})

const path = api.root.addResource('incoming')
path.addMethod('POST', integration, { methodResponses: [{ statusCode: '200' }] })

That’s it! 🎉 All of this may look complex and weird, but it’s only connecting fully managed services; there is no custom code involved and no custom business logic. The whole architecture scales to the service limits of SQS, API Gateway, and EventBridge. 🥰

Bunq Callback Integration

After the deployment of the resources, the AWS Cloud Development Kit will display the HTTP endpoint for the Amazon API Gateway; together with the created resource path and request method, you have the endpoint URL for the bunq Callback configuration; for example:

https://4ac3dc4c931.execute-api.eu-central-1.amazonaws.com/prod/incoming

Based on the existing guide to access the bunq API from your CLI, you can just configure a dedicated Callback endpoint URL for your MUTATION and PAYMENT events:

{
    "notification_filters": [
       {
          "category": "MUTATION",
          "notification_target":"https://4ac3dc4c931.execute-api.eu-central-1.amazonaws.com/prod/incoming"
       },
       {
          "category": "PAYMENT",
          "notification_target":"https://4ac3dc4c931.execute-api.eu-central-1.amazonaws.com/prod/incoming"
       }
    ]
} 

Make sure to comply with all additional required steps for the bunq API and configure the Callbacks afterwards:

# Generate signature for request payload
$ > openssl dgst -sha256 -sign bunq.pem -out notifications.sha256 notifications.json
$ > openssl enc -base64 -in notifications.sha256 -out notifications.sha256.base64

$ > export SIGNATURE=`tr -d '\n' < notifications.sha256.base64`

# Load signature as single line string to variable
$ > curl -X POST https://api.bunq.com/v1/user/1122334/monetary-account/3344556/notification-filter-url \
    -H "Content-Type: application/json" \
    -H "Cache-Control: no-cache" \
    -H "User-Agent: sbstjn.com/0.0.1" \
    -H "X-Bunq-Language: en_US" \
    -H "X-Bunq-Region: de_DE" \
    -H "X-Bunq-Client-Authentication: $TOKEN" \
    -H "X-Bunq-Client-Signature: $SIGNATURE" \
    --data @notifications.json

$ > curl -X POST https://api.bunq.com/v1/user/1122334/monetary-account/6677889/notification-filter-url \
    -H "Content-Type: application/json" \
    -H "Cache-Control: no-cache" \
    -H "User-Agent: sbstjn.com/0.0.1" \
    -H "X-Bunq-Language: en_US" \
    -H "X-Bunq-Region: de_DE" \
    -H "X-Bunq-Client-Authentication: $TOKEN" \
    -H "X-Bunq-Client-Signature: $SIGNATURE" \
    --data @notifications.json

Now, every PAYMENT event within your bunq account will be sent to an Amazon API Gateway. The Service Integration will process the HTTPS request and post a message to an Amazon SQS queue. The EventBridge Pipe will read new messages continuously from SQS to transform them into event payloads delivered to your EventBridge Bus.

The current EventBridge Pipe configuration has a basic transformation template configured:

[] 

inputTemplate: `{
  "message": "<$.body.message>"
}`,

[] 

Based on the bunq Callback payload, you can extend the template:

[]

inputTemplate: `{
  "category": "<$.body.NotificationUrl.category>",
  "event_type": "<$.body.NotificationUrl.event_type>",
  "payment": "<$.body.NotificationUrl.object.Payment.id>",
  "monetary_account_id": "<$.body.NotificationUrl.object.Payment.monetary_account_id>",
  "amount_currency": "<$.body.NotificationUrl.object.Payment.amount.currency>",
  "amount_value": "<$.body.NotificationUrl.object.Payment.amount.value>",
  "balance_after_mutation_currency": "<$.body.NotificationUrl.object.Payment.balance_after_mutation.currency>",
  "balance_after_mutation_value": "<$.body.NotificationUrl.object.Payment.balance_after_mutation.value>"
}`,

[]

This includes all relevant information for basic processing of your banking activities; next, this could be forwarded to a CloudWatch Log Group; this is already explained:

And with the data available in CloudWatch, we can use all the nice tools for querying and analysing data; But that’s something for another day …

Have a nice one! 🥰