Webhooks and Canopy
Activity that occurs in Canopy drives many downstream dependent actions. For example, sending email or text notifications to borrowers, executing logic in response to missed borrower payments, and more. All events that occur within the lifecycle of any borrowers on Canopy send webhook notifications for your dependent systems.
Organization-level Webhook URL
To assign your webhook URL to your Canopy organization, navigate to the “App Keys” page from the top right account dropdown. Under the Webhook URL section, paste your webhook URL into that input and hit the save button. Once configured in the Canopy UI, this URL will receive the following events.
Response Pattern
Events will hit your webhook URL in the following pattern:
sql{ "event": String, // The event name "data": Object, // The webhook payload "hmac_signature": String // The signature of the field "data" }
🧠
Your endpoint must respond with a
200
or 202
Other response codes and Retry
- Events associated with error codes
429
and5XX
are retried before sent to dead letter queue (DLQ).
- Events associated with error codes
1XX
,3XX
, and4XX
(excluding429
) aren't retried before sent to DLQ.
- Canopy will retry a single webhook event with exponential backoff, with the first retry 1 minute after the first failure, and will stop with the last retry approximately 24 hours after the first failure.
- The endpoint will pause if several error codes are received consecutively or if one
401
code is received
- Events stored in the dead letter queue (DLQ) are kept for 5 days, and you have the option to resend all DLQ events back to your webhook endpoint. A single put on the Canopy API route
/organization/subscribe/replay_failed_events
will replay all failed events to your webhook endpoint again.
Verifying HMAC Signatures
- You can verify the key on any HMAC library with encryption SHA256 and on Base64. This algorithm needs to be applied on the payload
data
field, and needs to be equal to thehmac_signature
field.
- You can retrieve your HMAC secret key via a GET call to Canopy:
- https://
{{hostname}}
/organization/subscribe/get_webhook_secret
- To manually verify, you can do so within an online SHA256 generation tool such as the one found here.
Example
javascriptconst CryptoJS = require("crypto-js"); const data = {...}; const HMACData = JSON.stringify(data); secret_key = '***'***'; const generatedHMAC = CryptoJS.HmacSHA256(HMACData, secret_key); const generatedHMACBase64 = CryptoJS.enc.Base64.stringify(generatedHMAC); console.log(generatedHMACBase64);
Event Payloads
Test Event schema:
test_event
json{ "welcome": "Welcome to Canopy!", "tutorials": "Check out our knowledge base at https://learn.canopyservicing.com", "api_reference": "Check out our API docs at https://docs.canopyservicing.com", "other_events": "See a full list of supported events at our [event definition page](https://www.notion.so/canopy1/External-Webhook-Events-d9e8bee140674b46a8eca6f426be3a86)", "sample_statement": { <Statement -- See Statement Schema Below> } }
Statement schema
statement_generation
json{ "event": "statement_generation", "data": { "account_id": { "type": "string", "description": "The Canopy-generated ID for the account", "example": "31mNprzLd2bKl6koVna68ARM" }, "statement_id": { "type": "string", "description": "The Canopy-generated ID for the statement", "example": "31mNprzLd2bKl6koVna68ARM" }, "effective_as_of_date": { "type": "string", "format": "date-time", "description": "The `Date-Time` you would like the system to return the data as of. IE tell me what the account information was as if I had asked on `2020-10-20 00:00:00 EST`. If empty it defaults to current time.", "example": "2018-07-20T09:10:14+00:00" }, "account_overview": { "type": "object", "properties": { "promo_purchase_window_inclusive_start": { "type": "string", "format": "date-time", "description": "If applicable, the `Date-Time` the purchase window for the account ends.", "example": "2015-08-31T10:49:44.461+00:00" }, "promo_purchase_window_exclusive_end": { "type": "string", "format": "date-time", "description": "If applicable, the `Date-Time` the purchase window for the account ends.", "example": "2015-08-31T10:49:44.461+00:00" }, "promo_inclusive_start": { "type": "string", "format": "date-time", "description": "If applicable, the `Date-Time` the promo period for the account starts.", "example": "2015-08-31T10:49:44.461+00:00" }, "promo_exclusive_end": { "type": "string", "format": "date-time", "description": "If applicable, the `Date-Time` the promo period for the account ends.", "example": "2015-08-31T10:49:44.461+00:00" }, "account_status": { "type": "string", "description": "The Status of the Account. Active upon account creation.", "example": "suspended", "default": "active" }, "account_status_subtype": { "type": "string", "description": "The subtype of the Status of the Account. Null upon account creation.", "example": "suspended-bankruptcy", "default": "" } } }, "open_to_buy": { "type": "object", "properties": { "credit_limit_cents": { "type": "integer", "description": "Total Amount (in cents) that this account can borrow.", "example": 400000, "default": 0 }, "total_charges_cents": { "type": "integer", "description": "Sum of all charges that occurred on the account since account origination.", "example": 800000, "default": 0 }, "available_credit_cents": { "type": "integer", "description": "The total available credit balance (in cents) for the account.", "example": 600000, "default": 0 }, "open_to_buy_cents": { "type": "integer", "description": "If applicable, the total amount of available funds for continued purchase following a purchase window pattern, where payments made do not replenish amount available for purchase.", "example": 600000, "default": 0 } } }, "cycle_summary": { "type": "object", "properties": { "cycle_inclusive_start": { "type": "string", "format": "date-time", "description": "The inclusive starting `Date-Time` that defines which transations are part of this statement." }, "cycle_exclusive_end": { "type": "string", "format": "date-time", "description": "The inclusive ending `Date-Time` that defines which transations are part of this statement." }, "cycle_charges_cents": { "type": "integer", "description": "Sum of all charges that occurred on the account during the billing cycle.", "example": 700000, "default": 0 }, "cycle_loans_cents": { "type": "integer", "description": "Sum of all loans amounts that were initiated on the account during the billing cycle.", "example": 700000, "default": 0 }, "cycle_charge_returns_cents": { "type": "integer", "description": "Sum of all returns that occurred on the account during the billing cycle.", "example": 100000, "default": 0 }, "cycle_payments_cents": { "type": "integer", "description": "Sum of all payments that occurred on the account during the billing cycle.", "example": 200000, "default": 0 }, "cycle_payment_reversals_cents": { "type": "integer", "description": "Sum of all payment reversal amounts that occurred on the account during the billing cycle.", "example": 200000, "default": 0 }, "cycle_debit_adjustments_cents": { "type": "integer", "description": "Sum of all debit adjustment amounts that occurred on the account during the billing cycle.", "example": 40000, "default": 0 }, "cycle_credit_adjustments_cents": { "type": "integer", "description": "Sum of all credit adjustment amounts that occurred on the account during the billing cycle.", "example": 53000, "default": 0 }, "cycle_interest_cents": { "type": "integer", "description": "Total interest accrued during the billing cycle.", "example": 110000, "default": 0 }, "cycle_am_interest_cents": { "type": "integer", "description": "The current AM interest balance of the line item. Canopy tracks interest during an amortization period separately from deferred interest accrued during a revolving period.", "example": 0, "default": 0 }, "cycle_deferred_interest_cents": { "type": "integer", "description": "Total interest accrued during the billing cycle.", "example": 300000, "default": 0 }, "cycle_am_deferred_interest_cents": { "type": "integer", "description": "Total interest accrued during the billing cycle. Canopy tracks deferred interest during an amortization period separately from deferred interest accrued during a revolving period.", "example": 300000, "default": 0 }, "cycle_late_fees_cents": { "type": "integer", "description": "Total late fees incurred during the billing cycle.", "example": 300000, "default": 0 }, "cycle_payment_reversals_fees_cents": { "type": "integer", "description": "Total payment reversal fees incurred during the billing cycle.", "example": 300000, "default": 0 }, "cycle_waived_deferred_interest_cents": { "type": "integer", "description": "Total deferred interest that was forgiven on the account during the billing cycle.", "example": 40000, "default": 0 } } }, "min_pay_due": { "type": "object", "required": ["min_pay_cents", "min_pay_due_at"], "properties": { "min_pay_cents": { "type": "integer", "description": "Total amount due for the billing cycle, summing cycle principal, interest, deferred interest, and fees outstanding.", "example": 160000 }, "min_pay_due_at": { "type": "string", "format": "date-time", "description": "The `Date-Time` the payment for this billing cycle is due.", "example": "2019-10-18T23:04:48.321+00:00" } } }, "additional_min_pay_details": { "type": "object", "required": [ "min_pay_charges_principal_cents", "min_pay_interest_cents", "min_pay_deferred_cents", "min_pay_fees_cents", "previous_min_pay_cents" ], "properties": { "min_pay_charges_principal_cents": { "type": "integer", "description": "Total principal due for the billing cycle.", "example": 100000 }, "min_pay_interest_cents": { "type": "integer", "description": "Total interest due for the billing cycle.", "example": 30000 }, "min_pay_am_interest_cents": { "type": "integer", "description": "Total am interest due for the billing cycle.", "example": 0 }, "min_pay_deferred_cents": { "type": "integer", "description": "Total deferred interest due for the billing cycle.", "example": 10000 }, "min_pay_am_deferred_interest_cents": { "type": "integer", "description": "The current AM deferred interest balance of the line item. Canopy tracks deferred interest during an amortization period separately from deferred interest accrued during a revolving period.", "example": 200, "default": 0 }, "min_pay_fees_cents": { "type": "integer", "description": "Total fees due for the billing cycle.", "example": 20000 }, "previous_min_pay_cents": { "type": "integer", "description": "Previous amounts due, including fees. This is a subset of min_pay_cents.", "example": 400000 } } }, "balance_summary": { "type": "object", "properties": { "charges_principal_cents": { "type": "integer", "description": "Total principal balance for the account.", "example": 900000, "default": 0 }, "loans_principal_cents": { "type": "integer", "description": "Total principal balance for loans for the account.", "example": 900000, "default": 0 }, "interest_balance_cents": { "type": "integer", "description": "Total interest balance for the account.", "example": 100000, "default": 0 }, "am_interest_balance_cents": { "type": "integer", "description": "Total AM interest balance for the account.", "example": 100000, "default": 0 }, "deferred_interest_balance_cents": { "type": "integer", "description": "Total deferred interest balance for the account.", "example": 700000, "default": 0 }, "am_deferred_interest_balance_cents": { "type": "integer", "description": "The total deferred interest balance (in cents) associated with the account.", "example": 40000, "default": 0 }, "fees_balance_cents": { "type": "integer", "description": "Total fee balance for the account.", "example": 700000, "default": 0 }, "total_balance_cents": { "type": "integer", "description": "The total balance (in cents) associated with the account.", "example": 400000, "default": 0 } } }, "payoff": { "type": "object", "properties": { "total_payoff_cents": { "type": "integer", "description": "The total amount needed to pay off the loan at this exact moment.", "example": 900000, "default": 0 }, "expected_remaining_payment_amount_cents": { "type": "integer", "description": "The total amount slated to pay off the account over the lifecycle of the loan.", "example": 1100000, "default": 0 } } } } }
Line Item Creation
line_item_create
json{ "event": "line_item_create", "data": { "object_type": "line_item", "object": { /* <post-line-item-rest-api-response> */ } } }
Line Item Update
line_item_update
json{ "event": "line_item_update", "data": { "object_type": "line_item", "changed_at": "2022-01-04T09:12:30+00:00", "changes": [ { "field_name": "line_item_overview.line_item_status", "previous_value": "PENDING", "new_value": "VALID" } ], "object": { /* <get-line-item-rest-api-response> */ } } }
Account Creation
account_create
sql{ "event": "account_create", "data": { "object_type": "account", "object": { /* <post-account-rest-api-response> */ } } }
Account Update
account_update
json{ "event": "account_update", "data": { "object_type": "account", "changed_at": "2022-01-04T09:12:30+00:00", "changes": [ { "field_name": "account_overview.account_status", "previous_value": "ACTIVE", "new_value": "SUSPENDED" }, { "field_name": "account_overview.account_status_subtype", "previous_value": "", "new_value": "DELINQUENT" }, ], "object": { /* <get-account-rest-api-response> */ } } }
Minimum Payment Due
minimum_payment_due
json{ "data": { "account_id": "d5c2aeee-6a9e-4057-b31f-c542b7a8a03d", "customer_id": "f0d5deb9-b04b-47ee-a09e-02597cdb3156", "statement_id": "5201", "min_pay_due_cents": 200, "total_balance_cents": 200 "min_pay_due_at": "2022-11-11T00:00:00-05:00", "time_stamp": "2022-11-11T00:00:00-05:00", }, "event": "minimum_payment_due", "hmac_signature": "6atLNzJ0KHumzJHwiqzHWO/4cLsYB/KhFppqhQ6IdV4=" }
Due Date
payment_due_date
json{ "event": "due_date", "data": { "account_id": "d5c2aeee-6a9e-4057-b31f-c542b7a8a03d", "customer_id": "f0d5deb9-b04b-47ee-a09e-02597cdb3156", "statement_id": "5201", "days_past_due": 7, "minimum_payment_due_date": "2022-11-11T00:00:00-05:00", "delinquent_as_of_date": "2022-11-16T00:00:00-05:00", "time_stamp": "2022-11-18T00:00:00-05:00" } }
Account Calculation Change :
account_calculation_change
json{ "data": { "account_id": 1000, "available_credit_cents": 50000, "effective_as_of": "2023-01-26T15:42:39.567559-05:00" }, "event": "account_calculations_change", "hmac_signature": "/vR5kHcsg66fkCasPLOb0IHYqb0vPj4tkNwi8EDZ5oM=" }
Configurable webhooks
Minimum Payment Due
minimum_payment_due
json{ "data": { "account_id": "d5c2aeee-6a9e-4057-b31f-c542b7a8a03d", "customer_id": "f0d5deb9-b04b-47ee-a09e-02597cdb3156", "statement_id": "5201", "min_pay_due_cents": 200, "total_balance_cents": 200 "min_pay_due_at": "2022-11-11T00:00:00-05:00", "time_stamp": "2022-11-11T00:00:00-05:00", }, "event": "minimum_payment_due", "hmac_signature": "6atLNzJ0KHumzJHwiqzHWO/4cLsYB/KhFppqhQ6IdV4=" }
Configuring
minimum_payment_due
webhook☝
Note this call needs to be made for each account
PUT /accounts/{{account_id}}/notification_config
{ "notification_type": "minimum_payment_due", "time_offset": "-1 days", ------effective from min pay due date "time": "00:00:00"------at which webhook will be triggered in product timezone
Payment Due Date
payment_due_date
javascript{ "event": "payment_due_date", "data": { "account_id": "d5c2aeee-6a9e-4057-b31f-c542b7a8a03d", "customer_id": "f0d5deb9-b04b-47ee-a09e-02597cdb3156", "statement_id": "5201", "days_past_due": 7, "min_pay_due_at": "2022-11-11T00:00:00-05:00", "min_pay_due_cents": 10000, "total_balance_cents": 1000000, "delinquent_as_of_date": "2022-11-16T00:00:00-05:00", "time_stamp": "2022-11-18T00:00:00-05:00", "min_pay_cents": 10000, "current_min_pay_cents": 10000, "unpaid_min_pay_cents": 0 } }
Minimum Payment Missed
minimum_payment_missed
javascript{ "data": { "account_id": "can_100031", "customer_id": "848", "statement_id": 77, "time_stamp": "2023-01-30 13:25:39.4009-05" }, "event": "minimum_payment_missed", "hmac_signature": "YQwoNrn8dFjcjyJ8cXNbvwDqRPYPkw5UJSHoIzDR0Ts=" }
Configuring the
minimum_payment_missed
webhook☝
Note this call needs to be made for each account
PUT /accounts/{{account_id}}/notification_config
javascript{ "notification_type": "minimum_payment_missed", "time_offset": "6 days", "time": "00:00:00" }
This webhook needs to be configured to be triggered on the date. Examples:
"time_offset": "0 days"
———> triggered on due date +grace days"time_offset": "6 days"
———> triggered on due date +grace days +6 days time_offset is always +ve and can be between 0 and cycle length
Account Delinquency
account_delinquency
javascript{ "data": { "account_id": "can_100030", "cure_amount": "3147", "customer_id": "848", "delinquent_as_of_date": "2023-01-30T13:23:00.487411-05:00", "number_of_days_delinquent": 0, "time_stamp": "2023-01-30T13:23:38.129534-05:00" }, "event": "account_delinquency", "hmac_signature": "dGIr5vD0KVYPLYcGsmld/wtfKym1+teUVZfVMmS+RQw=" }
Configuring the
account_delinquency
webhook☝
Note this call needs to be made for each account
PUT /accounts/{{account_id}}/notification_config
Note: Using this method, notifications will need to be configured for each individual account after account creation. Multiple instances of the webhook can be configured. If a periodic configuration exists for an account, it will be the only configuration that will be used.
Example Input Configuration Body
jsonIf using a time offset: { "notification_type": "account_delinquency", "time_offset": "7 days", "time": "00:00:00" } If using a periodic interval: { "notification_type": "account_delinquency", "periodic_interval": "DAILY" }
Payload Body Details:
- notification_type:
- type: string
- description: The type of notification whose delivery cadence is being defined for the account.
- example:
"account_delinquency"
- time_offset:
- type: string
- description: The interval offset from the originating activity from which the notification will be scheduled at. Supports the
ISO 8601
interval format. - For
account_delinquency
the originating activity is an account’s status becoming delinquent. - The offset can not be negative, and has no upper bound.
- Can not be used together with
periodic_interval
- example:
"3 days"
- If a statement due date is
01/25
and a time_offset is provided for3 days
, a notification will be scheduled for01/28
.
- time:
- type: string
- description: The time of day the notification will be scheduled for in the 24-hour time format.
- example:
"7:30:00"
- When the notification is offset to be scheduled at
01/20
and specifies a time for"7:30:00"
, the notification will be sent at07:30 AM
on the day of01/20
at the product’s time zone.
- periodic_interval:
- type: string
- description: The periodicity that will determine when this webhook will be sent, if account remains delinquent
- Can not be used together with
time_offset
ortime
- Webhook will only be sent when account becomes delinquent and if it still delinquent when callback should be sent
- example:
"DAILY"
- Currently only supports
"DAILY"
and"WEEKLY"