DEV Community

Nori
Nori

Posted on

Serverless Practice: Sending Trello Reminders to Your WhatsApp Using AWS

This solution enables you to receive WhatsApp notifications for Trello alerts, specifically for cards with an assigned due date.

Scenario

I needed a way to better organize my personal projects, including preparing for my AWS certification, managing medical appointments, social meetings, and various other tasks. With so many apps to juggle for work and multiple email accounts, I often missed or muted notifications from tools like Google Calendar. As a result, I frequently found myself forgetting important tasks.

Then it hit me: What app do I use every day that truly grabs my attention? The answer was clear—WhatsApp. For me, it’s the central hub for communication. Whenever I need to check something, I open WhatsApp and see all the tasks I still need to complete. After exploring the Trello integrations and power-ups, I found that the WhatsApp-to-Trello power-ups were charging 4 euros per month for each number, which would cost me 88 euros a year. That’s when I decided to create my own solution, practice serverless, and experiment with ways to minimize costs.

This solution uses the following workflow:

Scenario 1: Assign a new due date to a specific card.

  1. The user assigns a due date to a card.
  2. Trello webhooks send a POST request to our callback URL.
  3. The API Gateway receives the request and sends it to our Step Function.
  4. The Step Function filters the event for the 'update card' action and the 'due_date' sub-action.
  5. The Step Function triggers the Lambda function 'Create Event Scheduler'. This Lambda function creates a DynamoDB record with the card information and due date for the WhatsApp notification. It also uses the AWS SDK to create the EventBridge scheduler and stores the scheduler ARN in the DynamoDB.
  6. When the scheduler is triggered, it invokes the Notification WhatsApp lambda function, which uses a WhatsApp template that was previously created. This template invokes the Meta API to send the notification to my WhatsApp number.

Scenario 2: Update the due date for a specific card.

  1. The user updates the due date of a card.
  2. Trello webhooks send a POST request to our callback URL.
  3. The API Gateway receives the request and sends it to our Step Function.
  4. The Step Function filters the event for the 'update card' action and the 'due_date' sub-action.
  5. The Step Function triggers the Lambda function Create Event Scheduler, which checks if a record with a card ID already exists in DynamoDB. If so, only update the record with the new due date.
  6. The DynamoDB table is configured with DynamoDB Streams associated with the Lambda Update Scheduler EventBridge, which is triggered only when the specified attribute 'due_date' is modified. This Lambda updates the scheduler with the new date for the notification.
  7. When the scheduler is triggered, it invokes the Notification WhatsApp lambda function, which uses a WhatsApp template that was previously created and invokes the Meta API to send the notification to my WhatsApp number.

Pre-requirements

  • Trello Board created and some cards for testing
  • Whatsapp number to receive notifications
  • Meta developer account
  • AWS Account created and Admin Access.

Trello Configuration

To create a Power-Up, click on this link to access the Trello Power-Ups admin panel. On this page click on new.

Power-Up creation

Creating a New Power-Up

When creating a new Power-Up, you'll need to fill in the following fields:

  • App name: Enter a name for your Power-Up (e.g., "Whatsapp Trello Notifications")
  • Workspace: Select the workspace where you want to use this Power-Up (e.g., "work tasks")
  • Email: Enter the email address associated with your Trello account
  • Support contact: Provide a support email address where users can reach you. You can use the same email as above if preferred
  • Author: Enter the name of the author or your company name
  • Iframe connector URL: Leave this field empty since this solution does not use any graphical user interface

Once you've filled in all the required information, click the Create button to proceed with the Power-Up creation.

API Configuration

Once you've created the Power-Up, you will be redirected to the admin page where you can view your new Power-Up. Click on it to proceed with the next configuration steps.

When you are here please key in the API-KEY section.

Securely store your API Key and Secret—these credentials are required to interact with the Trello API. My recomendation use AWS Secret Manager

Webhook Setup

Now that you have the credentials, we need to create a Trello Webhook to receive all events from your Trello dashboard.

One of the requirements to create a webhook is having a callback URL. Trello will send a HEAD request to the endpoint you specify and will wait for a 200 response code to create and activate the webhook.

For more detailed information, please refer to the Trello Official Documentation.

To obtain your Callback URL, follow these steps:

  1. Import an AWS API Gateway
  2. Create an endpoint /events with the HTTP HEAD method and integrate it with a Lambda function that returns a 200 response code.

Go to your AWS Account and select API Gateway, create a new API Gateway and select the import option in the section REST API. Use the following JSON specification:

To obtain your Callback URL, follow these steps:

  1. Go to your AWS Account and navigate to API Gateway
  2. Click on Create API and select REST API
  3. Choose Import from Swagger option
  4. Copy and paste the following JSON specification in the import dialog:
  5. Click on CREATE API
{
  "swagger": "2.0",
  "info": {
    "description": "API created to send WhatsApp reminders for Trello tasks that need to be completed or to trigger reminders",
    "version": "2026-01-01T10:58:10Z",
    "title": "trelloWhatsapp notification"
  },
  "basePath": "/events",
  "schemes": [
    "https"
  ],
  "paths": {
    "/events": {
      "head": {
        "produces": [
          "application/json"
        ],
        "responses": {
          "200": {
            "description": "200 response",
            "schema": {
              "$ref": "#/definitions/Empty"
            }
          }
        }
      },
      "post": {
        "consumes": [
          "application/json"
        ],
        "produces": [
          "application/json"
        ],
        "responses": {
          "200": {
            "description": "200 response",
            "schema": {
              "$ref": "#/definitions/Empty"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "Empty": {
      "type": "object",
      "title": "Empty Schema"
    }
  },
  "x-amazon-apigateway-security-policy": "SecurityPolicy_TLS13_1_3_2025_09",
  "x-amazon-apigateway-endpoint-access-mode": "BASIC"
}
Enter fullscreen mode Exit fullscreen mode

Now you need to create a Lambda function that will respond to the HEAD request from Trello with a 200 status code.

Follow these steps:

  1. Go to your AWS Account and navigate to Lambda
  2. Click on Create function
  3. Select Author from scratch
  4. Configure the following:
    • Function name: Enter a name for your Lambda function (e.g., lambda-response-head-trello)
    • Runtime: Select your preferred runtime language. This example uses Python 3.x, but you can choose any supported language
    • Architecture: Select x86_64
  5. Click Create function

Once the function is created, you'll use the default code provided by AWS, which is sufficient for responding to Trello's HEAD request verification.

Now you need to integrate the Lambda function you just created with the API Gateway HEAD endpoint.

Follow these steps:

  1. Go back to your API Gateway console
  2. Select the HEAD endpoint under /events
  3. Click on Edit integration

  1. In the integration configuration page:
    • Select Lambda as the integration type
    • Choose the Lambda function you created in the previous step (lambda-response-head-trello)
    • Click Save

Once saved, your API Gateway HEAD endpoint is now integrated with your Lambda function. This will allow Trello to verify your webhook callback URL by receiving a 200 response.

Now you need to deploy your API Gateway to get the invoke URL that will be used as your Trello webhook callback URL.

Follow these steps:

  1. In your API Gateway console, click on Deploy API
  2. Configure the deployment:
    • Stage: Select Create new stage
    • Stage name: Enter a name for your stage (e.g., development)
    • Click Deploy

Get Your Callback URL

Once deployed, you need to obtain your invoke URL to use as the Trello webhook callback URL.

Follow these steps:

  1. In your API Gateway console, click on the / (root)
  2. Click on /events
  3. Click on Invoke URL
  4. Copy and save this URL—this is your callback URL that you'll use in the Trello webhook configuration

Your invoke URL will look like this:

https://{api-id}.execute-api.us-east-1.amazonaws.com/development/events
Enter fullscreen mode Exit fullscreen mode

Save this URL as you'll need it in the next step to create the Trello webhook.

To create the webhook and configure your automation, you need to obtain the Trello board ID.

Follow these steps:

  1. Go to your Trello board
  2. Select a card and click on it to open it
  3. Click on the three dots (•••) in the top right corner of the card
  4. Click on Share
  5. Click on Export as JSON
  6. Copy the JSON data and save it

From the exported JSON, you can find:

  • Board ID: The unique identifier for your Trello board called idBoard save this value we will need it on the next step

Create Trello Webhook using Bruno

Now that you have all the necessary parameters, you'll create the Trello webhook using Bruno (an API client similar to Postman).

Parameters collected:

  • Callback URL: Your API Gateway invoke URL
  • API Key: Your Trello API Key
  • API Token: Your Trello API Token
  • Model ID: Your Trello board ID

Step 1: Create a Bruno Collection and Environment Variables

  1. Open Bruno
  2. Click on Create Collection and name it trello-whatsapp-notifications
  3. Right-click on the collection and select Settings
  4. Go to the Environments tab
  5. Click on Create Environment and name it development
  6. Add the following environment variables with the values you collected:
{
  "CallbackUrl": "https://{api-id}.execute-api.us-east-1.amazonaws.com/development/events",
  "APIKey": "YOUR_TRELLO_API_KEY",
  "APIToken": "YOUR_TRELLO_API_TOKEN",
  "ModelId": "YOUR_TRELLO_BOARD_ID"
}
Enter fullscreen mode Exit fullscreen mode
  1. Click Save

Step 2: Create Webhook Request in Bruno

  1. In your Bruno collection, click on Create Request
  2. Name it Create Trello Webhook
  3. Set the request method to POST
  4. Enter the following URL:
https://api.trello.com/1/tokens/{{APIToken}}/webhooks/
Enter fullscreen mode Exit fullscreen mode
  1. In the Body tab, select JSON and add:
{
  "key": "{{APIKey}}",
  "callbackURL":  "{{CallbackUrl}}",
  "idModel": "{{ModelId}}",
  "description":  "Trello board events"
}
Enter fullscreen mode Exit fullscreen mode

DynamoDB

DynamoDB is used to store the state of all notifications. This database helps manage the status of notifications and provides the Lambda function that sends WhatsApp notifications with all the necessary information to build the message. The Create Event Scheduler Lambda function is responsible for populating the DynamoDB table.

The primary key for the DynamoDB table is cardID. Here is the DynamoDB table structure:

Attribute Type Description
cardID String (Primary Key) Unique identifier for the Trello card
card_name String Name of the Trello card
board_name String Name of the Trello board
list_name String Name of the list containing the card
card_url String URL link to the Trello card
due_to_date String Due date in ISO 8601 format
member_creator String Name of the member who created the card
hours_before_due Number Hours before due date to send the notification
notification_type String Type of notification (e.g., 1_DAY_BEFORE)
notification_date String Calculated date/time when the notification should be sent
schedule_name String Name of the EventBridge scheduler
schedule_arn String ARN of the EventBridge scheduler
schedule_status String Status of the scheduler (e.g., ENABLED, DISABLED)
notification_sent Boolean Whether the notification has been sent
notification_sent_at String Timestamp when the notification was sent
timestamp String Creation timestamp of the record

Example DynamoDB Item:

{
  "cardID": "6956438536af465e3d40b380",
  "card_name": "Programacion certificacion de terraform",
  "board_name": "working tasks",
  "list_name": "Lista de tareas",
  "card_url": "https://trello.com/c/RscETyKh",
  "due_to_date": "2026-02-02T10:58:00.000Z",
  "member_creator": "Eliana Alejandro Morales Lopez",
  "hours_before_due": 24,
  "notification_type": "1_DAY_BEFORE",
  "notification_date": "2026-02-01T10:58:00+00:00",
  "schedule_name": "trello-reminder-6956438536af465e3d40b380-1769943480",
  "schedule_arn": "arn:aws:scheduler:us-east-1:724772097129:schedule/default/trello-reminder-6956438536af465e3d40b380-1769943480",
  "schedule_status": "ENABLED",
  "notification_sent": true,
  "notification_sent_at": "2026-01-07T14:23:54.799628",
  "timestamp": "2026-01-01T10:59:09.672Z"
}
Enter fullscreen mode Exit fullscreen mode

DynamoDB Streams Configuration

DynamoDB Streams is an AWS service that captures item-level modifications in a DynamoDB table and sends notifications about these changes. In this solution, DynamoDB Streams monitors changes to the due_to_date attribute. When this attribute is updated, DynamoDB Streams triggers the Lambda Update Scheduler EventBridge function.

This Lambda function receives the event from DynamoDB Streams containing the cardId of the modified item. It then:

  1. Retrieves the current scheduler ARN from DynamoDB
  2. Deletes the existing EventBridge scheduler
  3. Creates a new scheduler with the updated due date
  4. Updates the scheduler ARN in the DynamoDB table

This ensures that every time the due date is modified, the notification schedule is automatically updated with the new date.

To configure DynamoDB Streams with your Lambda function:

  1. Go to your DynamoDB table in the AWS console
  2. Click on the Exports and streams tab
  3. Under DynamoDB Streams, click Enable

  1. Select New and old images as the stream specification

AWS Lambda Functions

This solution uses three Lambda functions to handle different parts of the workflow:

Update Scheduler Lambda (DynamoDB Streams)

This Lambda function is triggered by DynamoDB Streams when the due_to_date attribute of a card is updated. It automatically updates the EventBridge scheduler with the new due date.

Workflow

  1. Receives an event from DynamoDB Streams containing the modified cardID
  2. Retrieves the current scheduler ARN and old due date from DynamoDB
  3. Deletes the existing EventBridge scheduler
  4. Calculates the new notification date based on the updated due date
  5. Creates a new EventBridge scheduler with the updated date
  6. Updates the DynamoDB record with the new scheduler ARN and schedule name

Environment Variables

The Update Scheduler Lambda function requires the following environment variables to be configured:

Variable Description
TABLE_NAME The name of your DynamoDB table where card notification records are stored. Used to retrieve and update card information and scheduler details.
SCHEDULE_ROLE_ARN The ARN of the IAM role that EventBridge Scheduler will assume when executing scheduled tasks. This role must have permissions to invoke the WhatsApp Notification Lambda function.
TARGET_LAMBDA_ARN The ARN of the WhatsApp Notification Lambda function that will be invoked by the EventBridge scheduler when the notification time arrives.

How to set environment variables:

  1. Go to your Lambda function in the AWS console
  2. Click on the Configuration tab
  3. Select Environment variables from the left menu
  4. Click on Edit
  5. Click on Add environment variable for each variable
  6. Enter the variable name and its corresponding value
  7. Click Save

IAM Policy

The Update Scheduler Lambda function requires permissions to interact with CloudWatch Logs, EventBridge Scheduler, and DynamoDB services. Attach the following policy to your Lambda execution role:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CloudWatchLogsPermissions",
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:us-east-1:724772097129:*"
        },
        {
            "Sid": "CloudWatchLogsStreamPermissions",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:us-east-1:724772097129:log-group:/aws/lambda/lambda-update-scheduler-eventbridge:*"
        },
        {
            "Sid": "EventBridgeSchedulerPermissions",
            "Effect": "Allow",
            "Action": [
                "scheduler:CreateSchedule",
                "scheduler:GetSchedule",
                "scheduler:DeleteSchedule",
                "scheduler:UpdateSchedule"
            ],
            "Resource": "arn:aws:scheduler:us-east-1:724772097129:schedule/default/trello-reminder-*"
        },
        {
            "Sid": "DynamoDBPermissions",
            "Effect": "Allow",
            "Action": [
                "dynamodb:GetItem",
                "dynamodb:UpdateItem",
                "dynamodb:Query"
            ],
            "Resource": "arn:aws:dynamodb:us-east-1:724772097129:table/TrelloNotificationEvents"
        },
        {
            "Sid": "DynamoDBStreamsPermissions",
            "Effect": "Allow",
            "Action": [
                "dynamodb:GetRecords",
                "dynamodb:GetShardIterator",
                "dynamodb:DescribeStream",
                "dynamodb:ListStreams",
                "dynamodb:ListShards"
            ],
            "Resource": "arn:aws:dynamodb:us-east-1:724772097129:table/TrelloNotificationEvents/stream/*"
        },
        {
            "Sid": "PassRoleToScheduler",
            "Effect": "Allow",
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "arn:aws:iam::724772097129:role/eventbridgeSchedulerRole",
            "Condition": {
                "StringEquals": {
                    "iam:PassedToService": "scheduler.amazonaws.com"
                }
            }
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode
import json
import boto3
from datetime import datetime, timedelta
import os

# AWS Clients
scheduler_client = boto3.client('scheduler')
dynamodb = boto3.resource('dynamodb')

# Environment Variables
TABLE_NAME = os.environ.get('TABLE_NAME', 'TrelloNotificationEvents')
TARGET_LAMBDA_ARN = os.environ.get('TARGET_LAMBDA_ARN')
SCHEDULE_ROLE_ARN = os.environ.get('SCHEDULE_ROLE_ARN')

def lambda_handler(event, context):
    """
    Triggered by DynamoDB Streams when due_to_date is updated.
    Updates the EventBridge scheduler with the new due date.
    """

    try:
        print(f"📥 Received DynamoDB Streams event: {json.dumps(event)}")

        # Process each DynamoDB stream record
        for record in event.get('Records', []):
            if record['eventName'] != 'MODIFY':
                print(f"⏭️ Skipping non-MODIFY event: {record['eventName']}")
                continue

            # Extract the card ID and new data
            dynamodb_record = record['dynamodb']
            card_id = dynamodb_record['Keys']['cardID']['S']

            # Get new and old image
            new_image = dynamodb_record.get('NewImage', {})
            old_image = dynamodb_record.get('OldImage', {})

            # Check if due_to_date was actually changed
            old_due_date_str = old_image.get('due_to_date', {}).get('S')
            new_due_date_str = new_image.get('due_to_date', {}).get('S')

            if old_due_date_str == new_due_date_str:
                print(f"ℹ️ Due date not changed for card {card_id}, skipping")
                continue

            print(f"🔄 Due date updated for card: {card_id}")
            print(f"   Old due date: {old_due_date_str}")
            print(f"   New due date: {new_due_date_str}")

            # Get the current scheduler information from the new image
            old_schedule_arn = old_image.get('schedule_arn', {}).get('S')
            old_schedule_name = old_image.get('schedule_name', {}).get('S')
            card_name = new_image.get('card_name', {}).get('S', 'Unknown')

            # Step 1: Delete the old schedule
            if old_schedule_name:
                try:
                    print(f"🗑️ Deleting old schedule: {old_schedule_name}")
                    scheduler_client.delete_schedule(
                        Name=old_schedule_name
                    )
                    print(f"✅ Old schedule deleted: {old_schedule_name}")
                except scheduler_client.exceptions.ResourceNotFoundException:
                    print(f"⚠️ Schedule not found: {old_schedule_name}")
                except Exception as e:
                    print(f"⚠️ Error deleting schedule: {str(e)}")

            # Step 2: Parse the new due date
            new_due_date = datetime.fromisoformat(new_due_date_str.replace('Z', '+00:00'))
            now = datetime.now(new_due_date.tzinfo)

            # Calculate time until due date
            time_until_due = new_due_date - now

            print(f"⏰ New due date: {new_due_date.isoformat()}")
            print(f"📊 Time until due: {time_until_due}")

            # Step 3: Determine notification date based on new due date
            if time_until_due <= timedelta(hours=12):
                print(f"⚠️ New due date is less than 12 hours away, schedule not updated")

                # Update DynamoDB to mark schedule as deleted
                table = dynamodb.Table(TABLE_NAME)
                table.update_item(
                    Key={'cardID': card_id},
                    UpdateExpression='SET schedule_status = :status, notification_sent = :sent',
                    ExpressionAttributeValues={
                        ':status': 'CANCELLED',
                        ':sent': True
                    }
                )
                continue

            elif time_until_due <= timedelta(days=1):
                notification_date = new_due_date - timedelta(hours=12)
                notification_type = "12_HOURS_BEFORE"
                hours_before = 12
                print(f"📅 New due date is within 24 hours, scheduling for 12 hours before")

            else:
                notification_date = new_due_date - timedelta(days=1)
                notification_type = "1_DAY_BEFORE"
                hours_before = 24
                print(f"📅 New due date is more than 24 hours away, scheduling for 1 day before")

            # Verify notification date is not in the past
            if notification_date <= now:
                print(f"⚠️ New notification date is in the past: {notification_date}")

                table = dynamodb.Table(TABLE_NAME)
                table.update_item(
                    Key={'cardID': card_id},
                    UpdateExpression='SET schedule_status = :status, notification_sent = :sent',
                    ExpressionAttributeValues={
                        ':status': 'CANCELLED',
                        ':sent': True
                    }
                )
                continue

            # Step 4: Create a new schedule
            new_schedule_name = f"trello-reminder-{card_id}-{int(notification_date.timestamp())}"
            schedule_expression = notification_date.strftime('at(%Y-%m-%dT%H:%M:%S)')

            target_payload = {
                'cardID': card_id
            }

            try:
                print(f"📅 Creating new schedule: {new_schedule_name}")

                new_schedule_response = scheduler_client.create_schedule(
                    Name=new_schedule_name,
                    Description=f"Updated reminder for Trello card: {card_name} ({notification_type})",
                    ScheduleExpression=schedule_expression,
                    ScheduleExpressionTimezone='UTC',
                    FlexibleTimeWindow={
                        'Mode': 'OFF'
                    },
                    Target={
                        'Arn': TARGET_LAMBDA_ARN,
                        'RoleArn': SCHEDULE_ROLE_ARN,
                        'Input': json.dumps(target_payload)
                    },
                    State='ENABLED'
                )

                new_schedule_arn = new_schedule_response.get('ScheduleArn')

                print(f"✅ New schedule created: {new_schedule_name}")
                print(f"📍 New schedule ARN: {new_schedule_arn}")

                # Step 5: Update DynamoDB with the new scheduler information
                table = dynamodb.Table(TABLE_NAME)

                table.update_item(
                    Key={'cardID': card_id},
                    UpdateExpression='SET schedule_name = :sname, schedule_arn = :sarn, schedule_status = :status, notification_date = :ndate, notification_type = :ntype, hours_before_due = :hours, due_to_date = :duedate',
                    ExpressionAttributeValues={
                        ':sname': new_schedule_name,
                        ':sarn': new_schedule_arn,
                        ':status': 'ENABLED',
                        ':ndate': notification_date.isoformat(),
                        ':ntype': notification_type,
                        ':hours': hours_before,
                        ':duedate': new_due_date_str
                    }
                )

                print(f"✅ DynamoDB updated with new schedule: {card_id}")

            except scheduler_client.exceptions.ConflictException as e:
                print(f"⚠️ Schedule already exists: {str(e)}")
                return {
                    'statusCode': 409,
                    'body': json.dumps({
                        'error': 'Schedule already exists',
                        'message': str(e)
                    })
                }

        return {
            'statusCode': 200,
            'body': json.dumps({
                'message': 'Schedules updated successfully',
                'processedRecords': len(event.get('Records', []))
            })
        }

    except Exception as e:
        print(f"❌ Error processing DynamoDB Streams event: {str(e)}")
        import traceback
        traceback.print_exc()

        return {
            'statusCode': 500,
            'body': json.dumps({
                'error': str(e)
            })
        }
Enter fullscreen mode Exit fullscreen mode

#### Associate lambda with the DynamoDB Streams

  1. Go to your Lambda Update Scheduler EventBridge function
  2. Click on ConfigurationTriggers
  3. Click Add trigger and select DynamoDB
  4. Select your DynamoDB table and stream
  5. Click Add

2. Create Event Scheduler Lambda

This Lambda function processes incoming events from the Trello webhook. It uses the cardId as the primary key to check if a notification already exists in DynamoDB.

Workflow:

  1. Receives the JSON payload from the Trello webhook
  2. Checks if a record with the cardId already exists in DynamoDB
  3. If the notification doesn't exist:
    • Creates a new EventBridge scheduler with the card's due date
    • Stores the scheduler ARN in DynamoDB
  4. If the notification already exists:
    • Updates the due_to_date attribute in the DynamoDB record (which triggers DynamoDB Streams)

Environment variables:
The Create Event Scheduler Lambda function requires the following environment variables to be configured:

Variable Description
SCHEDULE_ROLE_ARN The ARN of the IAM role that EventBridge Scheduler will assume when executing scheduled tasks. This role must have permissions to invoke the WhatsApp Notification Lambda function.
TABLE_NAME The name of your DynamoDB table where card notification records are stored. This is used to store and retrieve card information along with scheduler details.
TARGET_LAMBDA_ARN The ARN of the WhatsApp Notification Lambda function that will be invoked by the EventBridge scheduler when the notification time arrives.

Required IAM permissions:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CloudWatchLogsPermissions",
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:us-east-1:724772097129:*"
        },
        {
            "Sid": "CloudWatchLogsStreamPermissions",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:us-east-1:724772097129:log-group:/aws/lambda/lambda-create-record-notification-trello:*"
        },
        {
            "Sid": "EventBridgeSchedulerPermissions",
            "Effect": "Allow",
            "Action": [
                "scheduler:CreateSchedule",
                "scheduler:GetSchedule",
                "scheduler:DeleteSchedule"
            ],
            "Resource": "arn:aws:scheduler:us-east-1:724772097129:schedule/default/trello-reminder-*"
        },
        {
            "Sid": "DynamoDBPermissions",
            "Effect": "Allow",
            "Action": [
                "dynamodb:PutItem",
                "dynamodb:GetItem",
                "dynamodb:Query",
                "dynamodb:UpdateItem"
            ],
            "Resource": "arn:aws:dynamodb:us-east-1:724772097129:table/TrelloNotificationEvents"
        },
        {
            "Sid": "PassRoleToScheduler",
            "Effect": "Allow",
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "arn:aws:iam::724772097129:role/eventbridgeSchedulerRole",
            "Condition": {
                "StringEquals": {
                    "iam:PassedToService": "scheduler.amazonaws.com"
                }
            }
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Create Event Scheduler Lambda Code

Here is the complete code for the Create Event Scheduler Lambda function:

import json
import boto3
from datetime import datetime, timedelta
import os

# AWS Clients
scheduler_client = boto3.client('scheduler')
dynamodb = boto3.resource('dynamodb')

# Environment Variables
TABLE_NAME = os.environ.get('TABLE_NAME', 'TrelloNotificationEvents')
TARGET_LAMBDA_ARN = os.environ.get('TARGET_LAMBDA_ARN')  # ARN of the Lambda that will send notifications
SCHEDULE_ROLE_ARN = os.environ.get('SCHEDULE_ROLE_ARN')  # Role for EventBridge Scheduler

def lambda_handler(event, context):
    """
    Receives a Trello event and creates a schedule in EventBridge
    to execute 1 day before the due date (or 12 hours if due date is very soon)
    """

    try:
        # Extract data from the event
        action = event.get('action', {})
        card_data = action.get('data', {}).get('card', {})

        card_id = card_data.get('id')
        card_name = card_data.get('name')
        due_date_str = card_data.get('due')

        if not card_id or not due_date_str:
            return {
                'statusCode': 400,
                'body': json.dumps('Missing cardID or due date')
            }

        print(f"🔍 Processing card: {card_name} (ID: {card_id})")

        # Parse the due date
        due_date = datetime.fromisoformat(due_date_str.replace('Z', '+00:00'))

        # Get the current time
        now = datetime.now(due_date.tzinfo)

        # Calculate the time difference between now and due date
        time_until_due = due_date - now

        print(f"⏰ Due date: {due_date.isoformat()}")
        print(f"📊 Time until due: {time_until_due}")

        # Determine when to send the notification
        if time_until_due <= timedelta(hours=12):
            # If due date is less than 12 hours away, don't schedule anything
            print(f"⚠️ Due date is less than 12 hours away, notification not scheduled")
            return {
                'statusCode': 200,
                'body': json.dumps({
                    'message': 'Due date is too soon (< 12 hours), notification not scheduled',
                    'cardID': card_id,
                    'dueDate': due_date_str,
                    'hoursUntilDue': time_until_due.total_seconds() / 3600
                })
            }
        elif time_until_due <= timedelta(days=1):
            # If due date is less than 24 hours but more than 12, schedule for 12 hours before
            notification_date = due_date - timedelta(hours=12)
            notification_type = "12_HOURS_BEFORE"
            print(f"📅 Due date is within 24 hours, scheduling for 12 hours before")
        else:
            # If due date is more than 24 hours away, schedule for 1 day before
            notification_date = due_date - timedelta(days=1)
            notification_type = "1_DAY_BEFORE"
            print(f"📅 Due date is more than 24 hours away, scheduling for 1 day before")

        # Verify that the notification date is not in the past
        if notification_date <= now:
            print(f"⚠️ Notification date is in the past: {notification_date}")
            return {
                'statusCode': 200,
                'body': json.dumps({
                    'message': 'Calculated notification date is in the past',
                    'cardID': card_id,
                    'notificationDate': notification_date.isoformat(),
                    'currentTime': now.isoformat()
                })
            }

        # Calculate time until notification
        time_until_notification = notification_date - now
        hours_until_notification = time_until_notification.total_seconds() / 3600

        print(f"⏰ Notification scheduled for: {notification_date.isoformat()}")
        print(f"⏳ Time until notification: {hours_until_notification:.2f} hours")

        # Create a unique name for the schedule
        schedule_name = f"trello-reminder-{card_id}-{int(notification_date.timestamp())}"

        # Date format for EventBridge Scheduler (at expression)
        schedule_expression = notification_date.strftime('at(%Y-%m-%dT%H:%M:%S)')

        # Payload to be sent to the notification Lambda
        target_payload = {
            'cardID': card_id
        }

        # 1️⃣ FIRST: Create the schedule in EventBridge
        print(f"📅 Creating schedule: {schedule_name}")

        schedule_response = scheduler_client.create_schedule(
            Name=schedule_name,
            Description=f"Reminder for Trello card: {card_name} ({notification_type})",
            ScheduleExpression=schedule_expression,
            ScheduleExpressionTimezone='UTC',
            FlexibleTimeWindow={
                'Mode': 'OFF'
            },
            Target={
                'Arn': TARGET_LAMBDA_ARN,
                'RoleArn': SCHEDULE_ROLE_ARN,
                'Input': json.dumps(target_payload)
            },
            State='ENABLED'
        )

        # Get the ARN of the created schedule
        schedule_arn = schedule_response.get('ScheduleArn')

        print(f"✅ Schedule created: {schedule_name}")
        print(f"📍 Schedule ARN: {schedule_arn}")

        # 2️⃣ THEN: Save to DynamoDB with the schedule_arn
        table = dynamodb.Table(TABLE_NAME)
        timestamp = action.get('date')

        # Build the card URL
        card_short_link = card_data.get('shortLink')
        card_url = f"https://trello.com/c/{card_short_link}" if card_short_link else ""

        dynamodb_item = {
            'cardID': card_id,
            'timestamp': timestamp,
            'card_name': card_name,
            'card_url': card_url,
            'due_to_date': due_date_str,
            'notification_date': notification_date.isoformat(),
            'notification_type': notification_type,
            'hours_before_due': 12 if notification_type == "12_HOURS_BEFORE" else 24,
            'board_name': action.get('data', {}).get('board', {}).get('name', ''),
            'list_name': action.get('data', {}).get('list', {}).get('name', ''),
            'member_creator': action.get('memberCreator', {}).get('fullName', ''),
            'schedule_name': schedule_name,
            'schedule_arn': schedule_arn,
            'schedule_status': 'ENABLED'
        }

        table.put_item(Item=dynamodb_item)

        print(f"✅ Saved to DynamoDB: {card_id}")
        print(f"🎯 Payload for notification: {target_payload}")

        return {
            'statusCode': 200,
            'body': json.dumps({
                'message': 'Schedule created and saved successfully',
                'scheduleName': schedule_name,
                'scheduleArn': schedule_arn,
                'notificationDate': notification_date.isoformat(),
                'notificationType': notification_type,
                'hoursBeforeDue': 12 if notification_type == "12_HOURS_BEFORE" else 24,
                'hoursUntilNotification': round(hours_until_notification, 2),
                'cardID': card_id,
                'cardName': card_name,
                'dueDate': due_date_str
            })
        }

    except scheduler_client.exceptions.ConflictException as e:
        # If the schedule already exists
        print(f"⚠️ Schedule already exists: {str(e)}")
        return {
            'statusCode': 409,
            'body': json.dumps({
                'error': 'Schedule already exists',
                'message': str(e)
            })
        }

    except Exception as e:
        print(f"❌ Error: {str(e)}")
        import traceback
        traceback.print_exc()

        return {
            'statusCode': 500,
            'body': json.dumps({
                'error': str(e),
                'cardID': card_id if 'card_id' in locals() else None
            })
        }
Enter fullscreen mode Exit fullscreen mode

3. WhatsApp Notification Lambda

This Lambda function is invoked by the EventBridge scheduler when the due date is one day before. It retrieves the card information from DynamoDB and sends a WhatsApp notification using the Meta WhatsApp API.

Workflow

  1. Receives the cardID from the EventBridge scheduler
  2. Retrieves the card information from DynamoDB using the cardID
  3. Fetches WhatsApp credentials from AWS Parameter Store (Phone ID, API Token, and recipient phone number)
  4. Formats the due date into a readable format (DD/MM/YYYY HH:MM)
  5. Constructs the WhatsApp message using the trello_notification template with dynamic parameters
  6. Sends the WhatsApp message via Meta WhatsApp API
  7. Updates the DynamoDB record to mark the notification as sent with the current timestamp
  8. Returns a success response with the message details

Environment Variables

The WhatsApp Notification Lambda function requires the following environment variables to be configured:

Variable Description
TABLE_NAME The name of your DynamoDB table where card notification records are stored. Used to retrieve card information and update the notification status.
WHATSAPP_PHONE_ID_PARAMETER The AWS Parameter Store key name for the WhatsApp Phone ID (e.g., /whatsappApiNumberId). This ID is required to send messages via the Meta WhatsApp API.
WHATSAPP_TOKEN_PARAMETER The AWS Parameter Store key name for the WhatsApp API Token (e.g., /whatsappToken). This is the authentication token for the Meta WhatsApp API.
RECIPIENT_PHONE_PARAMETER The AWS Parameter Store key name for the recipient phone number (e.g., /WhatsAppNumberEliana). This is the phone number that will receive the WhatsApp notifications in international format.

IAM Policy

The WhatsApp Notification Lambda function requires permissions to interact with CloudWatch Logs, DynamoDB, and AWS Systems Manager Parameter Store. Attach the following combined policy to your Lambda execution role:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CloudWatchLogsPermissions",
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:us-east-1:724772097129:*"
        },
        {
            "Sid": "CloudWatchLogsStreamPermissions",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:us-east-1:724772097129:log-group:/aws/lambda/lambda-whatsapp-notification:*"
        },
        {
            "Sid": "DynamoDBAccess",
            "Effect": "Allow",
            "Action": [
                "dynamodb:GetItem",
                "dynamodb:UpdateItem"
            ],
            "Resource": "arn:aws:dynamodb:us-east-1:724772097129:table/TrelloNotificationEvents"
        },
        {
            "Sid": "SSMParameterAccess",
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameter"
            ],
            "Resource": [
                "arn:aws:ssm:us-east-1:724772097129:parameter/whatsappApiNumberId",
                "arn:aws:ssm:us-east-1:724772097129:parameter/whatsappToken",
                "arn:aws:ssm:us-east-1:724772097129:parameter/WhatsAppNumberEliana"
            ]
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode
import json
import boto3
import os
import requests
from datetime import datetime
from botocore.exceptions import ClientError

# AWS Clients
dynamodb = boto3.resource('dynamodb')
ssm_client = boto3.client('ssm')

# Environment Variables
TABLE_NAME = os.environ.get('TABLE_NAME')
WHATSAPP_PHONE_ID_PARAMETER = os.environ.get('WHATSAPP_PHONE_ID_PARAMETER')
WHATSAPP_TOKEN_PARAMETER = os.environ.get('WHATSAPP_TOKEN_PARAMETER')
RECIPIENT_PHONE_PARAMETER = os.environ.get('RECIPIENT_PHONE_PARAMETER')

def get_parameter(parameter_name):
    """
    Retrieves a parameter from AWS Parameter Store
    """
    try:
        response = ssm_client.get_parameter(
            Name=parameter_name,
            WithDecryption=False
        )
        return response['Parameter']['Value']
    except ClientError as e:
        print(f"❌ Error getting parameter {parameter_name} from Parameter Store: {str(e)}")
        raise

def get_whatsapp_phone_id():
    """
    Gets the WhatsApp Phone ID from Parameter Store
    """
    try:
        return get_parameter(WHATSAPP_PHONE_ID_PARAMETER)
    except ClientError:
        print(f"⚠️ Using default WHATSAPP_PHONE_ID: 926715197193624")
        return '926715197193624'

def get_recipient_phone():
    """
    Gets the recipient phone number from Parameter Store
    """
    try:
        return get_parameter(RECIPIENT_PHONE_PARAMETER)
    except ClientError:
        print(f"⚠️ Using default RECIPIENT_PHONE: 573123234567")
        return '573123234567'

def get_whatsapp_token():
    """
    Gets the WhatsApp API token from Parameter Store
    """
    try: 
        return get_parameter(WHATSAPP_TOKEN_PARAMETER)
    except ClientError as e: 
        print(f"❌ Error getting token from Parameter Store: {str(e)}")
        raise

def format_date(date_str):
    """
    Formats ISO date to readable Spanish format
    Example: 2026-02-02T10:58:00.000Z -> 02/02/2026 a las 10:58
    """
    try:
        date_obj = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
        return date_obj.strftime('%d/%m/%Y a las %H:%M')
    except Exception as e: 
        print(f"⚠️ Error formatting date: {str(e)}")
        return date_str

def send_whatsapp_message(token, phone_number, card_name, board_name, list_name, due_date, card_url, whatsapp_phone_id):
    """
    Sends a WhatsApp message using the trello_notification template with dynamic parameters
    """
    url = f"https://graph.facebook.com/v23.0/{whatsapp_phone_id}/messages"

    headers = {
        'Authorization': f'Bearer {token}',
        'Content-Type': 'application/json'
    }

    payload = {
        "messaging_product": "whatsapp",
        "to": phone_number,
        "type": "template",
        "template": {
            "name": "trello_notification",
            "language": {
                "code": "es_CO"
            },
            "components": [
                {
                    "type": "header",
                    "parameters": [
                        {"type": "text", "text": "Recordatorio de Trello"}
                    ]
                },
                {
                    "type": "body",
                    "parameters": [
                        {"type": "text", "text": card_name},
                        {"type": "text", "text": board_name},
                        {"type": "text", "text": list_name},
                        {"type": "text", "text": due_date},
                        {"type": "text", "text": card_url}
                    ]
                }
            ]
        }
    }

    try:
        print(f"📤 Sending WhatsApp message to {phone_number}")
        print(f"📤 Payload: {json.dumps(payload)}")
        response = requests.post(url, headers=headers, json=payload, timeout=10)
        print(f"📥 Response status: {response.status_code}")
        print(f"📥 Response body: {response.text}")
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"❌ Error sending WhatsApp message: {str(e)}")
        if hasattr(e, 'response') and e.response is not None:
            print(f"Response: {e.response.text}")
        raise

def lambda_handler(event, context):
    """
    Receives the cardID from the scheduler, queries DynamoDB, and sends a WhatsApp notification
    """

    try:
        print(f"📥 Received event: {json.dumps(event)}")

        # Extract cardID from the event
        card_id = event.get('cardID')

        if not card_id: 
            print("❌ Missing cardID in payload")
            return {
                'statusCode': 400,
                'body': json.dumps({'error': 'Missing cardID in payload'})
            }

        print(f"🔍 Getting item with cardID: {card_id}")

        # Get the record from DynamoDB
        table = dynamodb.Table(TABLE_NAME)

        response = table.get_item(
            Key={'cardID': card_id}
        )

        if 'Item' not in response:
            print(f"⚠️ Card not found in database: {card_id}")
            return {
                'statusCode': 404,
                'body': json.dumps({'error': 'Card not found in database'})
            }

        # Get card data
        card_data = response['Item']

        card_name = card_data.get('card_name', 'Sin nombre')
        due_date_str = card_data.get('due_to_date', '')
        card_url = card_data.get('card_url', '')
        board_name = card_data.get('board_name', '')
        list_name = card_data.get('list_name', '')

        print(f"📋 Card found: {card_name}")
        print(f"📅 Due date: {due_date_str}")
        print(f"🔗 URL: {card_url}")

        # Format the due date
        due_date_formatted = format_date(due_date_str)

        # Get parameters from Parameter Store
        print("🔐 Getting parameters from Parameter Store...")
        whatsapp_phone_id = get_whatsapp_phone_id()
        recipient_phone = get_recipient_phone()
        token = get_whatsapp_token()

        # Send WhatsApp message with individual parameters
        print(f"📤 Sending message to {recipient_phone}...")
        result = send_whatsapp_message(
            token, 
            recipient_phone, 
            card_name,
            board_name,
            list_name,
            due_date_formatted,
            card_url,
            whatsapp_phone_id
        )

        print(f"✅ Message sent successfully: {result}")

        # Update DynamoDB record to mark notification as sent
        table.update_item(
            Key={'cardID': card_id},
            UpdateExpression='SET notification_sent = :val, notification_sent_at = :date',
            ExpressionAttributeValues={
                ':val': True,
                ':date': datetime.utcnow().isoformat()
            }
        )

        print(f"✅ DynamoDB updated with notification status for cardID: {card_id}")

        return {
            'statusCode': 200,
            'body': json.dumps({
                'message': 'Notification sent successfully',
                'cardID': card_id,
                'recipientPhone': recipient_phone,
                'result': result
            })
        }

    except Exception as e:
        print(f"❌ Error in lambda_handler: {str(e)}")
        import traceback
        traceback.print_exc()

        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }
Enter fullscreen mode Exit fullscreen mode

WhatsApp API Configuration

Create and Verify Meta Business Account

To use the WhatsApp API, you need to create and verify a Meta Business Account. This account will allow you to access the WhatsApp Cloud API and manage your WhatsApp Business integration.

Step 1: Access Meta Business Platform

  1. Go to the Meta Business Platform Login
  2. If you don't have a Meta account, click on Create Account
  3. Enter your email address and create a secure password
  4. Click Create Account

Step 2: Set Up Your Business Account

  1. After logging in, you'll be prompted to create a Meta Business Account
  2. Fill in the following information:
    • Business Account Name: Enter a name for your business (e.g., "Trello WhatsApp Notifications")
    • Your Name: Enter your full name
    • Business Email: Use a business email address
    • Business Phone Number: Enter your phone number
    • Country: Select your country
  3. Click Create Account

Step 3: Verify Your Business Information

  1. Once your account is created, go to SettingsBusiness Information
  2. Complete the following verification steps:

    • Verify your email address: Check your email inbox for a verification link from Meta and click it
    • Add a phone number: Enter and verify your phone number
    • Upload business documents (if required):
      • Government-issued ID
      • Business registration documents
      • Proof of address
  3. Click Submit for Review once all information is complete

Step 4: Wait for Approval

  • Meta will review your information (usually takes 24-48 hours)
  • You'll receive an email notification once your account is verified
  • Once approved, your business account will be fully active

Step 5: Access the Developer Platform

  1. After verification, go to Meta Developers
  2. Log in with your Meta Business Account credentials
  3. Click on My Apps in the top menu
  4. Click Create App to create a new app for WhatsApp integration

Step 6: Create a New App

Create a Meta App for WhatsApp Integration

Follow these steps to create a Meta App that will be used for WhatsApp Cloud API integration.

Step 1: App Details

  1. In the App Details section, fill in the following information:
    • Email: Enter a valid email address associated with your Meta Business Account
    • App Name: Enter a name for your app (e.g., "Trello WhatsApp Notifications")
  2. Click Next

Step 2: Select Use Case

  1. In the Use Cases section, you'll see different options for what you want to do with your app
  2. Select Connect with customers through WhatsApp
  3. This option allows you to use the WhatsApp Cloud API to send messages
  4. Click Next

Step 3: Select Business Portfolio

  1. In the Business section, select your business portfolio
  2. This is the Meta Business Account you created and verified in the previous steps
  3. Make sure to select the correct portfolio associated with your business
  4. Click Next

Step 4: Check Requirements

  1. In the Requirements section, Meta will display any requirements you need to fulfill
  2. Review each requirement carefully:
    • Business Account Verification: Make sure your business account is verified
    • Phone Number: Ensure you have a valid phone number registered
    • Other Requirements: Complete any additional requirements listed
  3. If all requirements are met, you can proceed directly to the next step
  4. If there are unmet requirements, complete them first
  5. Click Next once all requirements are satisfied

Step 5: Summary and Complete Setup

  1. In the Summary section, you'll see a review of all the information you provided:
    • App Name
    • Use Case
    • Business Portfolio
    • Requirements Status
  2. Review all the details to ensure they are correct
  3. Click Go to Dashboard to complete the app creation and access your app dashboard

Configure Use Case for WhatsApp Integration

Now that you've created your Meta App, you need to configure the WhatsApp use case to enable the WhatsApp Cloud API integration.

Step 1: Access Your App

  1. Go to Meta Developers
  2. Click on My Apps in the top menu
  3. Select the app you just created (e.g., "Trello WhatsApp Notifications")
  4. You'll be taken to your app dashboard

Step 2: Navigate to Use Cases

  1. In your app dashboard, look for the Use Cases section in the left sidebar
  2. Click on Use Cases
  3. You'll see a list of available use cases for your app

Step 3: Add a New Use Case

  1. Click on Add Use Case button
  2. A list of available use cases will appear

Step 4: Select WhatsApp Use Case

  1. From the list of available use cases, find and select Connect with customers through WhatsApp
  2. This use case enables you to send messages to your customers via WhatsApp using the Cloud API
  3. Click Add to add this use case to your app

Step 4: Create System User and Generate Permanent Access Token

Go to this link Click here to access your Meta Business Settings

You need to create a permanent access token to use in the WhatsApp Business platform. This token will be used to authenticate API requests to send WhatsApp messages.

Step 1: Navigate to System Users

  1. Go to Business Settings in your Meta Business Account
  2. In the left sidebar, click on System Users
  3. You'll see a list of existing system users (if any)

Step 2: Create a New System User

  1. Click on the Add + button in the top right corner
  2. A dialog will appear asking for system user details
  3. Fill in the following information:
    • System User Name: Enter a name for your system user (e.g., "Trello WhatsApp Bot")
    • System User Role: Select Admin
  4. Click Create System User

Step 3: Assign Assets to the System User

  1. Select the system user you just created
  2. Click on Assign Assets button
  3. You'll be taken to the asset assignment page

Step 4: Assign Your App

  1. In the Apps section:
    • Select your WhatsApp app (e.g., "Trello WhatsApp Notifications")
    • Enable Manage app with Full Control permissions
  2. Click Assign Assets

Step 5: Assign Your WhatsApp Business Account

  1. In the WhatsApp Business Accounts section:
    • Select your WhatsApp Business Account
    • Enable Manage WhatsApp Business Accounts with Full Control permissions
  2. Click Assign Assets

Step 6: Generate Access Token

  1. Go back to the system user you created
  2. Click on Generate Token button
  3. A dialog will appear with token generation options
  4. Select your app

  1. Set the token expiration (choose Never for a permanent token)

  1. Select the following scopes (permissions) for your token:
    • business_management: Allows management of your business account
    • whatsapp_business_messaging: Allows sending WhatsApp messages
    • whatsapp_business_management: Allows management of WhatsApp Business Account

  1. Click Generate Token

Step 7: Copy and Secure Your Token

  1. Copy the generated token from the dialog
  2. Important: Save this token in a secure location immediately
  3. Store it in AWS Parameter Store as you'll need it for the Lambda function
  4. Never share this token publicly or commit it to version control

Security Best Practices:

⚠️ Secure Storage: Always store your token in AWS Parameter Store or AWS Secrets Manager with encryption enabled

⚠️ Token Expiration: Consider setting an expiration date for your token and rotating it periodically

⚠️ Access Control: Restrict who has access to this token. Never expose it in logs or error messages

⚠️ Revocation: If your token is compromised, immediately revoke it and generate a new one

Step 8: Store Token in AWS Parameter Store

Once you have your access token, store it securely in AWS Parameter Store:

  1. Go to AWS Systems ManagerParameter Store
  2. Click Create parameter
  3. Fill in the following details:
    • Name: /whatsappToken
    • Type: SecureString (for encryption)
    • Value: Paste your access token here
  4. Click Create parameter

This token will be used by the WhatsApp Notification Lambda function to authenticate API requests to the Meta WhatsApp API.

Token Validation

To verify your token is working correctly, you can test it by making a simple API request:

curl -X GET "https://graph.facebook.com/v18.0/me?fields=name,email&access_token=YOUR_ACCESS_TOKEN"
Enter fullscreen mode Exit fullscreen mode

If successful, you'll receive a JSON response with your business account information.

Create WhatsApp Message Template

You need to create a WhatsApp message template that will be used to send Trello notifications. This template must be approved by Meta before you can use it to send messages.

Step 1: Access Message Templates

  1. Click here to access the WhatsApp Message Templates manager
  2. You'll see a list of your existing message templates (if any)
  3. Click on Create Template button to create a new template

Step 2: Select Template Category

  1. In the Category section, select Utility
  2. Utility templates are used for transactional messages like notifications, reminders, and alerts
  3. This is the appropriate category for Trello notification reminders
  4. Click Next

Step 3: Name Your Template and Select Language

  1. Fill in the template details:
    • Template Name: Enter a name for your template (e.g., trello_notification)
    • Language: Select your preferred language (e.g., Spanish - es_CO, English - en_US)
  2. The template name should be descriptive and in lowercase with underscores
  3. Click Next

Step 4: Create Template Body with Variables

Now you'll create the message body that will be sent to users. This template will include 5 dynamic variables that will be replaced with actual Trello card information.

Variables:

  1. {{1}} - Task name (tarea)
  2. {{2}} - Board name (tablero)
  3. {{3}} - List name (lista)
  4. {{4}} - Due date (vence)
  5. {{5}} - Card URL (url)

In the Message Body section, enter the following template text:

🎯 *Trello Reminder Notification*

Task: {{1}}
Board: {{2}}
List: {{3}}
Due Date: {{4}}

View Task: {{5}}

Please complete this task on time!
Enter fullscreen mode Exit fullscreen mode

Example with Sample Values:

For Meta's review process, provide example values:

  • Task: "Complete project documentation"
  • Board: "Working Tasks"
  • List: "In Progress"
  • Due Date: "02/02/2026 at 10:58 AM"
  • Card URL: "https://trello.com/c/RscETyKh"

The final message will look like:

🎯 *Trello Reminder Notification*

Task: Complete project documentation
Board: Working Tasks
List: In Progress
Due Date: 02/02/2026 at 10:58 AM

View Task: https://trello.com/c/RscETyKh

Please complete this task on time!
Enter fullscreen mode Exit fullscreen mode

Click Next to continue

Step 6: Review and Submit for Approval

  1. Review all template details:
    • Template Name: trello_notification
    • Category: Utility
    • Language: Your selected language
    • Body with variables
    • Header (if added)
    • Footer (if added)
  2. Make sure all the information is correct
  3. Click Submit for Review to send your template to Meta for approval

Important Notes About Template Approval

⏱️ Approval Time: Meta's review process typically takes 24-48 hours, but can take up to 2 days in some cases

📋 Review Criteria: Meta will review your template to ensure it:

  • Complies with WhatsApp's messaging policies
  • Doesn't contain prohibited content
  • Uses appropriate language for the category
  • Follows WhatsApp's brand guidelines

Approval Status: You'll receive a notification when your template is approved. You can also check the status in the Message Templates manager

Step 5: Get WhatsApp Phone Number ID

Meta provides a test phone number for your WhatsApp Business Account. You need to retrieve the Phone Number ID and store it securely in AWS Parameter Store.

Step 1: Navigate to Phone Numbers Section

  1. Go to your WhatsApp Business Account dashboard
  2. In the left sidebar, click on Phone Numbers
  3. You'll see the test phone number that Meta provides for your account
  4. It will be displayed in a format like: +1 (XXX) XXX-XXXX or similar

Step 2: Select Your WhatsApp Test Phone Number

  1. Click on the phone number to view its details
  2. A panel will open showing the phone number configuration and information
  3. Look for the Phone Number ID field in the details panel
  4. This is the unique identifier you need to store in AWS Parameter Store

Step 3: Copy the Phone Number ID

  1. In the phone number details, locate the Phone Number ID field
  2. This is a numeric ID (e.g., 926715197193624)
  3. Click on the copy icon next to the Phone Number ID or manually select and copy it
  4. Save this value temporarily as you'll need it in the next step

Step 4: Store Phone Number ID in AWS Parameter Store

Now you'll store the Phone Number ID securely in AWS Parameter Store:

  1. Go to AWS Systems Manager in your AWS console
  2. Click on Parameter Store in the left sidebar
  3. Click Create parameter
  4. Fill in the following details:
    • Name: /whatsappApiNumberId
    • Type: String
    • Description: "WhatsApp Business Phone Number ID for Trello notifications"
    • Value: Paste the Phone Number ID you copied from Meta (e.g., 9267151971935456)
  5. Click Create parameter

Step 5: Verify Parameter Storage

  1. You should see a success message confirming the parameter was created
  2. Navigate to Parameter Store to verify your parameter appears in the list
  3. You should see:
    • Parameter Name: /whatsappApiNumberId
    • Type: String
    • Last Modified Date: Current date

AWS Parameter Store Configuration

Your AWS Parameter Store should now contain:

Parameter Name Type Value Description
/whatsappApiNumberId String Your Phone Number ID WhatsApp Business Phone Number ID
/whatsappToken SecureString Your Access Token WhatsApp API Access Token
/WhatsAppNumberEliana String Recipient Phone Number Recipient phone number for notifications

Important Notes

⚠️ Phone Number ID Format: The Phone Number ID is a numeric string without any special characters or formatting

⚠️ Test vs Production: Meta provides a test phone number for development and testing. For production use, you'll need to set up a dedicated phone number

⚠️ Recipient Phone Number: Make sure the recipient phone number is also stored in Parameter Store as /WhatsAppNumberEliana in international format (e.g., 573123234567)

⚠️ Security: Keep your Phone Number ID and Access Token secure. These credentials authenticate all API requests to the WhatsApp API

Next Steps

Now that you have all three credentials stored in AWS Parameter Store:

  1. ✅ Phone Number ID: /whatsappApiNumberId
  2. ✅ Access Token: /whatsappToken
  3. ✅ Recipient Phone Number: /WhatsAppNumberEliana

Step 6: Authorize WhatsApp Number for Testing

Now you need to authorize your WhatsApp number to receive and review messages from your assigned test number. This step is required to test the WhatsApp notification integration.

Step 1: Access Use Cases Configuration

  1. Go to your Meta App Dashboard
  2. In the left sidebar, click on Use Cases
  3. Find and click on Connect with customers through WhatsApp
  4. Click on API Setup or Configuration
  5. You'll see the WhatsApp API configuration page

Step 2: Add a Phone Number

  1. In the API configuration page, look for the section Phone Numbers or Add Phone Number
  2. Click on the dropdown menu that says Select a phone number to add or Add Phone Number
  3. This dropdown will show available phone numbers or an option to add a new one

Step 3: Enter Your Phone Number

  1. In the dropdown or input field, enter your phone number in international format
  2. Format: Country code + phone number
  3. Make sure to include:
    • Country code (without the + symbol)
    • Phone number without spaces or special characters
  4. Click Next or Add

Step 4: Enter Verification Code

  1. Meta will send a verification code to the phone number you provided
  2. You'll receive an whatsapp message with a code (usually 6 digits)
  3. Enter the verification code in the dialog box that appears
  4. Click Verify to confirm your phone number

Create Step Function

Template Definition

# filepath: template.yaml
Resources:
  StateMachine80541e5e:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      Definition:
        Comment: A description of my state machine
        StartAt: trello event type
        States:
          trello event type:
            Type: Choice
            Choices:
              - Next: Lambda Invoke
                Condition: >-
                  {% (($states.input.action.type) = ("updateCard") and
                  ($states.input.action.display.translationKey) =
                  ("action_added_a_due_date")) %}
            Default: Success
          Lambda Invoke:
            Type: Task
            Resource: arn:aws:states:::lambda:invoke
            Output: '{% $states.result.Payload %}'
            Arguments:
              FunctionName: ${lambdainvoke_FunctionName_ccca2377}
              Payload: '{% $states.input %}'
            Retry:
              - ErrorEquals:
                  - Lambda.ServiceException
                  - Lambda.AWSLambdaException
                  - Lambda.SdkClientException
                  - Lambda.TooManyRequestsException
                IntervalSeconds: 1
                MaxAttempts: 3
                BackoffRate: 2
                JitterStrategy: FULL
            End: true
          Success:
            Type: Succeed
        QueryLanguage: JSONata
      DefinitionSubstitutions:
        lambdainvoke_FunctionName_ccca2377: >-
          arn:aws:lambda:us-east-1:724772097129:function:lambda-create-record-notification-trello:$LATEST
      RoleArn:
        Fn::GetAtt:
          - Role470f0f3f
          - Arn
      StateMachineName: StateMachine80541e5e
      StateMachineType: STANDARD
      EncryptionConfiguration:
        Type: AWS_OWNED_KEY
      LoggingConfiguration:
        Level: 'OFF'
        IncludeExecutionData: false

  Role470f0f3f:
    Type: AWS::IAM::Role
    Properties:
      RoleName: StepFunctions_IAM_ROLE_trelloWhatsappStateMachine78bed1bd
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: states.amazonaws.com
            Action: sts:AssumeRole
      MaxSessionDuration: 3600

  Policy2bd1232d:
    Type: AWS::IAM::RolePolicy
    Properties:
      PolicyName: DynamoDBTableContentScopedAccessPolicycf373a3d
      RoleName:
        Ref: Role470f0f3f
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - dynamodb:GetItem
              - dynamodb:PutItem
              - dynamodb:UpdateItem
              - dynamodb:DeleteItem
            Resource:
              - >-
                arn:aws:dynamodb:us-east-1:724772097129:table/TrelloNotificationEvents

  Policyb218aea1:
    Type: AWS::IAM::RolePolicy
    Properties:
      PolicyName: LambdaInvokeScopedAccessPolicyff7103aa
      RoleName:
        Ref: Role470f0f3f
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - lambda:InvokeFunction
            Resource:
              - >-
                arn:aws:lambda:us-east-1:724772097129:function:lambda-create-record-notification-trello:*
          - Effect: Allow
            Action:
              - lambda:InvokeFunction
            Resource:
              - >-
                arn:aws:lambda:us-east-1:724772097129:function:lambda-create-record-notification-trello

  Policy0c0067d6:
    Type: AWS::IAM::RolePolicy
    Properties:
      PolicyName: XRayAccessPolicya9ae136e
      RoleName:
        Ref: Role470f0f3f
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - xray:PutTraceSegments
              - xray:PutTelemetryRecords
              - xray:GetSamplingRules
              - xray:GetSamplingTargets
            Resource:
              - '*'

Outputs:
  StateMachineArn:
    Description: ARN of the Step Functions State Machine
    Value:
      Ref: StateMachine80541e5e
    Export:
      Name: TrelloWhatsappStateMachineArn

  StateMachineName:
    Description: Name of the Step Functions State Machine
    Value:
      Ref: StateMachine80541e5e

Enter fullscreen mode Exit fullscreen mode

Add the arn lambda for create lambda create scheduler.

Deploy CloudFormation Stack

aws cloudformation create-stack --stack-name trello-whatsapp-state-machine --template-body template.yaml --region us-east-1 --capabilities CAPABILITY_NAMED_IAM
Enter fullscreen mode Exit fullscreen mode

Step Functions Workflow

Once you deploy the CloudFormation stack, your Step Functions State Machine will be created and will appear in the AWS Step Functions console. The state machine implements a filtering mechanism to process only relevant Trello events.

State Machine Purpose

The Step Functions State Machine evaluates incoming Trello webhook events and determines whether they should be processed by the Lambda function. It acts as a gatekeeper, filtering events based on specific criteria:

  • Action Type: Must be updateCard
  • Action Display Translation Key: Must be action_added_a_due_date

If the event meets both conditions, it is passed to the Lambda function for processing. If not, the execution terminates successfully without further processing.

State Machine Workflow

Trello Webhook Event
        ↓
   ┌─────────────────┐
   │  Trello event   │
   │     type        │
   └────────┬────────┘
            ↓
   ┌─────────────────────────────────────────┐
   │ Check:                                  │
   │ - action.type == "updateCard"           │
   │ - action.display.translationKey ==      │
   │   "action_added_a_due_date"             │
   └────────┬─────────────────────┬──────────┘
            │ YES                 │ NO
            ↓                     ↓
   ┌──────────────────┐   ┌──────────────┐
   │  Lambda Invoke   │   │   Success    │
   │  (Process Card)  │   │  (Terminate) │
   └──────────────────┘   └──────────────┘
            ↓
   ┌──────────────────────────────────┐
   │ Create EventBridge Schedule      │
   │ Send WhatsApp Notification       │
   └──────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

State Machine Components

Choice State: "trello event type"

This state evaluates the incoming Trello webhook event against two conditions:

  1. Condition 1: action.type == "updateCard"

    • Ensures the event is for a card update action
    • Filters out other Trello actions like board updates, list changes, etc.
  2. Condition 2: action.display.translationKey == "action_added_a_due_date"

    • Ensures the card update is specifically a due date change
    • Filters out other card updates like name changes, description changes, etc.

Logic:

  • If BOTH conditions are TRUE → Route to "Lambda Invoke" state
  • If EITHER condition is FALSE → Route to "Success" state (terminate execution)

Lambda Invoke State

This state invokes the Lambda function that will:

  • Create an EventBridge scheduler for the notification
  • Store the card information in DynamoDB
  • Calculate the notification timing based on the due date

Success State

This state terminates the execution successfully when:

  • The event is not a card update
  • The event is a card update but not a due date change
  • Any other non-matching scenarios

Example Scenarios

Scenario 1: Due Date Added to Card (✅ PROCESSES)

{
  "action": {
    "type": "updateCard",
    "display": {
      "translationKey": "action_added_a_due_date"
    },
    "data": {
      "card": {
        "id": "5f8c7d4e3c2b1a9d",
        "name": "Complete project documentation",
        "due": "2026-02-02T10:58:00.000Z"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Result: ✅ Event passes both conditions → Lambda is invoked

Scenario 2: Card Name Changed (❌ DOES NOT PROCESS)

{
  "action": {
    "type": "updateCard",
    "display": {
      "translationKey": "action_changed_the_name"
    },
    "data": {
      "card": {
        "id": "5f8c7d4e3c2b1a9d",
        "name": "Updated card name"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Result: ❌ Event fails translation key condition → Execution terminates

Scenario 3: Board Created (❌ DOES NOT PROCESS)

{
  "action": {
    "type": "createBoard",
    "display": {
      "translationKey": "action_created_board"
    },
    "data": {
      "board": {
        "id": "5f8c7d4e3c2b1a9d",
        "name": "New Board"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Result: ❌ Event fails action type condition → Execution terminates

Benefits of This Filtering Approach

Efficient: Only processes relevant due date events
Cost-effective: Reduces unnecessary Lambda invocations
Reliable: Prevents processing incomplete or irrelevant data
Scalable: Handles high volume of Trello webhook events
Maintainable: Clear, declarative logic for event filtering

Monitoring the State Machine

You can monitor your Step Functions State Machine executions in the AWS console:

  1. Go to Step Functions in the AWS console
  2. Click on StateMachine80541e5e
  3. View execution history and logs
  4. Check which events passed the filter and which were rejected
  5. Debug any issues with event structure or conditions

Integration with Step Functions

The POST endpoint is integrated with AWS Step Functions, which orchestrates the workflow by filtering events and routing them to the appropriate Lambda function.

Configure API Gateway with Step Functions Integration

Now you need to configure the API Gateway POST endpoint to integrate directly with your Step Functions State Machine instead of using a Lambda function.

Step 1: Delete the Existing POST Method

  1. Go to your API Gateway console
  2. Select the POST method under /events
  3. Click Delete Method
  4. Confirm the deletion

Step 2: Create a New POST Method

  1. In your API Gateway console, select the /events resource
  2. Click on Create Method
  3. Select POST from the dropdown menu
  4. Click Create

Step 3: Configure POST Method Integration

  1. In the integration setup page, fill in the following:
    • Integration type: Select AWS Service
    • AWS Service: Select Step Functions
    • HTTP Method: Select POST
    • Action Type: Select StartSyncExecution (for synchronous execution)
    • Action: Enter StartSyncExecution
    • Execution Role ARN: Select or create an IAM role that allows API Gateway to invoke Step Functions
  2. In the Request section:
    • Set Request Body Passthrough to When there are no templates defined (recommended)
  3. Click Save

Step 4: Configure Request Mapping

  1. Click on the POST method you just created
  2. Click on Integration Request
  3. Expand the Mapping Templates section
  4. Click on Add mapping template
  5. Enter application/json as the content type
  6. In the template body, enter:
{
  "stateMachineArn": "arn:aws:states:us-east-1:724772097129:stateMachine:StateMachine80541e5e",
  "input": "$input.json('$')"
Enter fullscreen mode Exit fullscreen mode

click create y and deploy the api changes

Test the Integration with Trello

Now that your API Gateway is configured and deployed, you can test the complete integration by setting a due date on a Trello card.

Step 1: Add a Due Date to a Trello Card

  1. Go to your Trello Board
  2. Select a card you want to test with
  3. Click on the card to open it
  4. In the card details panel, click on Due Date
  5. Set a due date for the card (e.g., tomorrow at 10:00 AM)
  6. Click Save

Screenshot

Step 2: Receive WhatsApp Notification

When the scheduled notification time arrives based on your due date, you will receive a WhatsApp message on your authorized phone number with the following information:

  • 🎯 Task Name: The name of your Trello card
  • Board: The board the card belongs to
  • List: The list containing the card
  • Due Date: The formatted due date (DD/MM/YYYY HH:MM)
  • Link: Direct URL to the Trello card

Screenshot

How It Works

  1. Set Due Date in Trello: You add or update a due date on a Trello card
  2. Webhook Triggered: Trello sends a webhook event to your API Gateway endpoint
  3. Step Functions Filter: The Step Functions State Machine evaluates the event
  4. Lambda Processing: If the event matches the criteria, the Lambda function is invoked
  5. EventBridge Scheduler Created: An EventBridge scheduler is created for the notification time
  6. DynamoDB Storage: The card information is stored in DynamoDB
  7. Scheduled Execution: At the scheduled time, another Lambda function is triggered
  8. WhatsApp Message Sent: The WhatsApp notification is sent to your phone
  9. Status Updated: The DynamoDB record is updated to mark the notification as sent

Notification Timing

The notification will be sent based on the due date:

  • If due date is > 24 hours away: Notification sent 1 day before the due date
  • If due date is 12-24 hours away: Notification sent 12 hours before the due date
  • If due date is < 12 hours away: No notification is scheduled (event may have already passed)

Example Scenario

Card Details:

  • Card Name: "Complete project documentation"
  • Board: "Working Tasks"
  • List: "In Progress"
  • Due Date: February 5, 2026 at 10:00 AM

Notification Timing:

  • If set on February 3, 2026: Notification sent on February 4, 2026 at 10:00 AM (1 day before)
  • If set on February 4, 2026 at 11:00 AM: Notification sent on February 5, 2026 at 10:00 AM (< 24 hours, so 12 hours before if applicable)

WhatsApp Message Received:

🎯 Trello Reminder Notification

Task: Complete project documentation
Board: Working Tasks
List: In Progress
Due Date: 05/02/2026 at 10:00

View Task: https://trello.com/c/RscETyKh

Please complete this task on time!
Enter fullscreen mode Exit fullscreen mode

Update Due Date

You can also test the update functionality:

  1. Go back to the same Trello card
  2. Click on the due date to edit it
  3. Change it to a different date/time
  4. The DynamoDB Streams will trigger the Update Lambda
  5. The EventBridge scheduler will be updated with the new date
  6. You'll receive a WhatsApp notification at the new scheduled time

Bibliography and References

This document contains all the reference materials and documentation used in the development of the Trello WhatsApp Notifications system.

AWS Step Functions

AWS DynamoDB

Trello API Documentation

Meta WhatsApp Business API

Video Resources


Additional Resources

For more information about the technologies and services used in this project:

Top comments (0)