- Benmore Brief
- Posts
- LeafLink Webhooks: A Simple Integration with Pipedrive using AWS Lambda
LeafLink Webhooks: A Simple Integration with Pipedrive using AWS Lambda
This article will delve into how to integrate Leaflink with Pipedrive using webhooks and AWS Lambda Functions.
What is a Webhook?
Before we dive into LeafLink's implementation, it’s essential to understand what a webhook is. A webhook is a mechanism that allows one system to send real-time data to another system via HTTP requests, typically in the form of JSON. This is especially useful for applications that need to respond to events without continuously polling an API.
A Classic Use Case
Consider a common scenario involving a payment processor. When a user attempts to make a purchase—say, buying a pair of shoes online—they input their credit card details. The e-commerce system processes this information and forwards it to a payment processor for validation and transaction completion.
Once the transaction is processed, the system needs to be notified whether the payment was successful or failed. Instead of the e-commerce platform repeatedly querying the payment processor’s API (which could lead to unnecessary load and delays), the payment processor can send a webhook to the e-commerce system.
This webhook contains a JSON payload that includes the transaction status—allowing the e-commerce platform to respond appropriately, such as granting access to purchase confirmation or retrying the transaction.
LeafLink's Webhook System
LeafLink, leverages a similar webhook architecture to manage orders efficiently. Here’s how it works:
Order Status Lifecycle
When using LeafLink, orders progress through various statuses such as:
Submitted
Accepted
Fulfilled
Shipped
Cancelled
Rejected
Backorder
Each status represents a stage in the order’s lifecycle, and specific triggers cause the order to transition from one status to another. For example, an order is confirmed when the supplier acknowledges it, and it is marked as shipped once it leaves the warehouse.
Transition Notifications via Webhooks
To keep users informed of these transitions, LeafLink employs webhooks. Whenever an order status changes, LeafLink sends a JSON packet containing information about the order. This packet typically includes details such as the order ID, current status, and any relevant metadata.
This real-time notification system ensures that users are promptly informed of any changes, allowing them to manage their operations smoothly.
How to set up Webhooks on LeafLink
LeafLink Settings Configuration
From your LeafLink dashboard go to Settings > Company Information
Scroll down to Developer Access and Enable Developers Options Access and Webhooks
Scroll to the bottom, and press save.
Now you can go to Developer Options and select Create Webhook
You will now see a screen like this:
Great! Now we’re all set up, let’s set up an AWS Lambda Function to interpret the webhook and send it to Pipedrive.
Keep this tab open and go to your AWS account and log into the console. (https://aws.amazon.com/)
Setting up a Lambda Function
From your AWS console, search for “lamda“ and open the Lambda Service:
Select Create Function, give your function a name, and select your coding language of choice (for this tutorial we’ll be using Python 10).
Create the function.
From the function screen, select configuration.
From the left toolbar, select Function URL, then select Create function url.
Select an Auth Type of None, then press save to create the url for the function.
You can now see the function URL:
Copy the url.
Go back to LeafLink > Settings > Developer Options > Add Webhook
Name the webhook AWS TEST and paste your Lambda function URL under URL:
Press Save.
Great, now we just need to configure a couple of Lambda level environment variables!
Pipedrive API Key and LeafLink Signing Secret
Now, we need to set environment variables for the LeafLink Signing Secret and our Pipedrive API key.
First, let’s complete the Pipedrive API key.
Log into your Pipedrive account. (pipedrive.com)
Click your profile in the top right > personal preferences > API
Generate and copy the API key.
Go back to your Lamda function in AWS > Configuration > Environment Variables
Press Edit
Name your environment variable PIPEDRIVE and paste in the API key from Pipedrive:
Don’t press save yet, go back to your LeafLink Dashboard > Settings > Developer Options
Scroll to the bottom and under Company Webhook Key, Generate and Copy a Key:
Go back to the Lambda Environment Variable Page, press add environment variable, name it LEAFLINK_SECRET and paste the key. Press save.
You should now have two environment variables:
Finally! We’re ready to start writing some code to complete the integration.
Programming the Lambda Function
Now that we’re all set up, we essentially need to set this Lambda function to receive a webhook from LeafLink, extract and transform the data that we want, and forward it to Pipedrive.
Below is an overview of the logic we’ll be developing:
Structure of the Webhook
The first step in understanding how to interpret the webhook is by examining its JSON payload.
Here's an example of the LeafLink Webhook:
Warning - it’s a lot. 😂
{
"action": "edit",
"data": {
"buyer": null,
"corporate_address": {
"address": "XXX Street Name",
"city": "XXX City",
"state": "XX",
"unit_number": "",
"zipcode": "XXXXX"
},
"created_on": "2024-10-02T20:34:05.270631-06:00",
"credits": "0.00",
"customer": {
"address": "XXX Street Name",
"archived": false,
"business_identifier": null,
"business_license_name": null,
"city": "XXX City",
"county": "",
"created_on": "2022-11-08T16:13:26.923917-07:00",
"dba": null,
"delinquent": false,
"delivery_preferences": "",
"description": "",
"discount_percent": "0.00",
"ein": null,
"email": "[email protected]",
"external_id": null,
"id": 12870734268,
"license_number": "XXXX-XXXXX",
"license_type": {
"classification": "MIPS",
"display_type": "Adult Use",
"has_medical_line_items": false,
"id": 6,
"state": "XX",
"type": "Recreational",
"type_code": "REC"
},
"managers": [
{
"company": 10573492,
"company_staff_permissions": [
{
"id": 1013419065,
"permission": "can_access_api",
"permission_display": "Can Access Developer Options"
},
{
"id": 1019272066,
"permission": "can_edit_prices",
"permission_display": "Can Edit Prices"
},
{
"id": 1019275067,
"permission": "can_export_crm",
"permission_display": "Can Export CRM"
},
{
"id": 10192373068,
"permission": "can_manage_billing",
"permission_display": "Can Manage Billing"
},
{
"id": 1027319069,
"permission": "can_manage_crm",
"permission_display": "Can Manage Customers and Contacts"
},
{
"id": 10172349070,
"permission": "can_manage_fulfillment",
"permission_display": "Can Manage Fulfillment"
},
{
"id": 1019273071,
"permission": "can_manage_inventory",
"permission_display": "Can Manage Inventory"
},
{
"id": 1075319072,
"permission": "can_manage_orders_received",
"permission_display": "Can Manage Orders Received"
},
{
"id": 1019234073,
"permission": "can_access_sales_reports",
"permission_display": "Can Access Sales Reports"
}
],
"display_name": "Manager Name",
"email": "[email protected]",
"has_limited_access": false,
"id": 118467323,
"identification_number": null,
"is_account_owner": false,
"is_active": true,
"is_admin": true,
"is_poc": false,
"last_login": "2024-10-03 02:33:31.857648+00:00",
"phone": "XXX-XXX-XXXX",
"point_of_contact": true,
"position": "Sales Account Manager",
"selected_timezone": "America/XXX",
"user": "Manager Name ([email protected])",
"username": "[email protected]"
}
],
"name": "Customer Name",
"next_contact_date": null,
"nickname": null,
"notes": null,
"owner": {
"id": 10492322,
"licenses": [
{
"id": 11634435616,
"number": "XXXX-XXXXX",
"type": {
"classification": "MIPS",
"display_type": "Adult Use",
"has_medical_line_items": false,
"id": 6,
"state": "XX",
"type": "Recreational",
"type_code": "REC"
},
"warning": "expired"
},
{
"id": 136323445608,
"number": "XXXX-XXXXX",
"type": {
"classification": "MIPS",
"display_type": "Medical",
"has_medical_line_items": false,
"id": 3,
"state": "XX",
"type": "Medical",
"type_code": "MED"
},
"warning": null
},
{
"id": 184356245,
"number": "XXXX-XXXXX",
"type": {
"classification": "MIPS",
"display_type": "Adult Use",
"has_medical_line_items": false,
"id": 6,
"state": "XX",
"type": "Recreational",
"type_code": "REC"
},
"warning": null
}
],
"name": "Business Name"
},
"partner": null,
"payment_methods": [],
"payment_term": null,
"phone": null,
"price_schedule": null,
"service_zone": {
"created_on": "2024-01-02T19:50:53.669638-07:00",
"description": "",
"name": "Service Zone Name"
},
"shipping_charge": null,
"state": "XX",
"status": null,
"tags": [],
"tier": null,
"unit_number": "",
"website": null,
"zipcode": "XXXXX"
},
"delivery_address": {
"address": "XXX Street Name",
"city": "XXX City",
"state": "XX",
"unit_number": "",
"zipcode": "XXXXX"
},
"delivery_preferences": "",
"discount": "0.0000",
"discount_amount": "0.0000",
"discount_type": "%",
"external_id_buyer": null,
"external_id_seller": null,
"external_ids": {},
"facility_id": 887,
"final_tax": "0.00",
"internal_notes": null,
"manual": true,
"notes": "Send with Manager Friday",
"number": "Order-XXXXX",
"orderedproduct_set": [
{
"id": 7483940,
"inventory_item": {
"batch_id": null,
"facility_id": 487,
"id": 2529788,
"product_id": 2203451246
},
"is_sample": false,
"ordered_unit_price": "200.00",
"product": {
"id": 2201246,
"name": "Product Name 1",
"sku": "Product-SKU-1"
},
"quantity": "16.0000",
"sale_price": "0.00",
"unit_multiplier": 16
}
],
"paid": false,
"paid_date": null,
"payment_due_date": null,
"payment_methods": ["Check"],
"payment_term": "Net 30",
"sales_reps": [
{
"company": 234584,
"company_staff_permissions": [
{
"id": 234523,
"permission": "can_access_api",
"permission_display": "Can Access Developer Options"
},
{
"id": 485743,
"permission": "can_edit_prices",
"permission_display": "Can Edit Prices"
}
],
"display_name": "Manager Name",
"email": "[email protected]",
"has_limited_access": false,
"id": 483948,
"identification_number": null,
"is_account_owner": false,
"is_active": true,
"is_admin": true,
"is_poc": false,
"last_login": "2024-10-03 02:33:31.857648+00:00",
"phone": "XXX-XXX-XXXX",
"point_of_contact": true,
"position": "Sales Account Manager",
"selected_timezone": "America/XXX",
"user": "Manager Name ([email protected])",
"username": "[email protected]"
}
],
"selected_payment_option": {
"id": 234853,
"is_default": false,
"order_id": 483743,
"payment_method": {
"id": 2,
"method": "Check"
},
"payment_program": null,
"payment_strategy": null,
"payment_term": {
"code": "net30",
"days": 30,
"id": 4,
"term": "Net 30"
}
},
"seller": {
"id": 23453,
"licenses": [
{
"id": 12345,
"number": "XXXX-XXXXX",
"type": {
"classification": "MIPS",
"display_type": "Adult Use",
"has_medical_line_items": false,
"id": 3,
"state": "XX",
"type": "Recreational",
"type_code": "REC"
},
"warning": "expired"
}
],
"name": "Test Business"
},
"ship_date": null,
"shipping_charge": "0.00",
"shipping_details": null,
"short_id": "ShortID-XXXXX",
"status": "Accepted",
"subtotal": "2000.08",
"tax_amount": "0.000000",
"tax_type": "%",
"total": "2000.08",
"user": "[email protected]"
},
"type": "order"
}
Woah - yea it’s a large amount of data. So here’s a quick text breakdown of the payload:
Root level:
action: Type of action, in this case,
"edit"
.data: Contains all the main information related to the order.
data object:
buyer: Currently
null
.corporate_address: An object containing details like address, city, state, and zipcode.
created_on: The timestamp when the data was created.
credits: The amount of credits (currently
"0.00"
).customer: Contains customer information like address, license details, and manager info.
customer object:
address, city, state, zipcode: Basic contact details.
archived: Boolean field for archived status.
license_type: Details about the customer's license, including its classification, type, and state.
managers: A list of managers with their details (e.g., name, email, permissions).
manager object:
company_staff_permissions: Contains a list of permissions assigned to the manager.
display_name, email, phone, position: Personal details of the manager.
last_login: Timestamp of the manager's last login.
owner: Contains information about the owner of the business, including licenses.
delivery_address: Similar to the corporate address, includes details for the delivery location.
orderedproduct_set: A list of ordered products, each with details like product ID, price, and quantity.
payment_methods: Available payment methods for the transaction (e.g.,
"Check"
).selected_payment_option: Specific details about the selected payment term and method.
seller: Information about the seller, including business name and licenses.
subtotal, tax_amount, total: Financial details of the order.
user: Email of the user associated with the order.
For the purpose of this tutorial - we’re only going to be using:
"name": "Customer Name",
"total": "2000.08",
But please explore the rest of the data for common use cases like:
Changing status of the order in Pipedrive as its status changes on LeafLink
Syncing user’s deals on LeafLink with their deals on Pipedrive
Syncing customers across LeafLink and Pipedrive
Extracting what we need for Pipedrive
Now that we know what the webhook looks like, we can extract the customer name and deal total to create a deal on Pipedrive.
In our AWS Lambda function, using the following code snippet, we can extract the customer name and deal total from the webhook and use the Pipedrive API (https://developers.pipedrive.com/docs/api/v1) to create the deal:
A couple considerations for the code snippet below:
Feel free to test it with the example webhook above.
This is not production ready code.
It DOES NOT check for the webhook secret stored in LEAFLINK_SECRET
To incorporate more functionality check out the Pipedrive API reference: https://developers.pipedrive.com/docs/api/v1
If you manually are creating a deal, LeafLink sends webhooks if the draft is saved or you take a long time filling out the deal, there is code below to discard those webhooks.
The code snippet below DOES NOT take in to account deals changing statuses. It will create a new deal for every webhook that comes in. If you want to sync status changes, further configuration is need.
We recommend saving the LeafLink order ID from the webhook to a custom field in Pipedrive, and updating the existing deals based on the link.
import json
import hmac
import hashlib
import os
import urllib3
def lambda_handler(event, context):
# Discard draft webhooks
if event.get('action') == 'Draft':
return {
'statusCode': 200,
'body': json.dumps('Discarded the draft')
}
# Extracting order data
order_data = event.get('data')
customer_data = order_data.get('customer', {})
deal_name = customer_data.get('name')
deal_value = order_data.get('total')
# You can find this when you're logged into
# Pipedrive from the url as it is:
# yourdomain.pipedrive.com
company_domain = 'YOUR PIPEDRIVE DOMAIN'
api_token = os.environ['PIPEDRIVE']
deal = {
'title': deal_name,
'value': deal_value
}
url = f'https://{company_domain}.pipedrive.com/api/v1/deals?limit=3&api_token={api_token}'
# Create an instance of PoolManager
http = urllib3.PoolManager()
# Convert deal data to 'application/x-www-form-urlencoded' format
encoded_deal = urllib3.request.urlencode(deal).encode('utf-8')
# Make a POST request using urllib3 (AWS Lambda doesn't have the requests library installed by default.)
response = http.request(
'POST',
url,
body=encoded_deal,
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
# Check if the response status is 201 Created
if response.status == 201:
return {
'statusCode': 200,
'body': json.dumps('Deal Created')
}
else:
return {
'statusCode': response.status,
'body': json.dumps(f'Failed to create deal: {response.data.decode("utf-8")}')
}
Deploy the function!
And there you have it! You should see a deal created in your Pipedrive pipeline if you test with the sample webhook above, and should start seeing deals being created for order changes!