# Webhooks Brex APIs offer webhooks to notify you in real-time when certain events happen in your account. Webhooks are HTTPS requests made to an endpoint of your choice that you can then implement custom business logic for. For example, you can receive a webhook when a referral you made has activated. Your endpoint that receives this webhook can then process the payload of the webhook to automate some internal process you have based on this referral, such as updating records in your database, or sending the user a notification. ## Webhook event catalog Currently, the Brex API sends webhooks for the following events: | API | Event Type | Trigger description | | --- | --- | --- | | [Onboarding](/openapi/onboarding_api) | `REFERRAL_CREATED` | Triggered once a referral is created, whether by API or mobile/web | | [Onboarding](/openapi/onboarding_api) | `REFERRAL_ACTIVATED` | Triggered when a referred contact signs up for Brex using your referral link | | [Onboarding](/openapi/onboarding_api) | `REFERRAL_APPLICATION_STATUS_CHANGED` | Triggered when the product application status is changed | | [Payments](/openapi/payments_api) | `TRANSFER_PROCESSED` | Triggered when a transfer is successfully received | | [Payments](/openapi/payments_api) | `TRANSFER_FAILED` | Triggered when a transfer fails | | [Team](/openapi/team_api) | `USER_UPDATED` | Triggered when a user is updated | For more details on each event and their associated payloads, see the [Webhook Events API Reference](/openapi/webhooks_api/#tag/Webhook-Events). ## How to implement webhooks At a high level, in order to implement webhooks, you'll need to: 1. Register a URL to receive the webhooks using the [register webhook](/openapi/webhooks_api/#operation/createSubscription) endpoint. 2. Implement the registered URL as a public POST endpoint on your servers that accepts the webhook payload. This endpoint should: 3. Verify the signature of the request to make sure the webhook is from Brex. 4. Grab the relevant pieces of payload you need and then perform your business logic. ### Registering your new endpoint Hit the [register webhook endpoint](/openapi/webhooks_api/#operation/createSubscription) so Brex knows where to fire the webhooks. Provide it a URL and the events you wish to listen to in the payload. For example: ```javascript POST '/v1/webhooks' Payload { "url": "https://myapi.com/brex-webhook-processor" "event_types": [ "REFERRAL_CREATED", "REFERRAL_ACTIVATED", "TRANSFER_PROCESSED", "TRANSFER_FAILED" ] } ``` You can use the [Webhooks API subscriptions endpoints](/openapi/webhooks_api/#tag/Webhook-Subscriptions) at any time to unregister or update your webhooks. **Note:** Currently only one webhook endpoint can be registered per customer / `client_id`. However, each endpoint can be registered to listen to multiple `event_types` which your endpoint can then handle the logic for. ### Implementing your webhook receiving endpoint Now we need to implement the endpoint. First it should be publicly available on the web, and accept a `POST` with a `JSON` payload. #### Verify signature To make sure the webhook request was sent by Brex and not an impersonator, we highly recommend verifying the signature in the request headers against a signature you calculate with a secret. Each webhook payload contains the following headers: ```json Webhook-Id: the unique message identifier for the webhook message. This identifier is unique across all messages, but will be the same when the same webhook is being resent (e.g. due to a previous failure). Webhook-Timestamp: timestamp in seconds since epoch. Webhook-Signature: the Base64 encoded list of signatures (space delimited). ``` The content to sign is composed by concatenating the id, timestamp and raw payload, separated by the full-stop character (.). In code, it will look something like: ```javascript signed_content = `${webhook_id}.${webhook_timestamp}.${raw_request_body}` ``` The signature is sensitive to any changes, so even a small change in the body will cause the signature to be completely different. This means that you should not change the body in any way before verifying. Brex uses an HMAC with SHA-256 to sign its webhooks and you'll need to make a request to the `GET /v1/webhooks/secrets` endpoint to get the signing secret(s). During key rotation, this endpoint will return 2 keys, both the new key, and the key that will be revoked soon. Your application should check against all of the keys to validate webhook payload and if the validation passes for any of the keys, the webhook payload is valid. To calculate the computed signature, HMAC the `signed_content` with a base64 decoded version of the secret. For example: ```javascript const secret_string = "MCDBG16t42aV0Esn2sHyv1kRaip1LPEC"; const signed_content = `${webhook_id}.${webhook_timestamp}.${raw_request_body}` const base64DecodedSecret = Buffer.from(secret_string, 'base64'); const hmac = crypto.createHmac('sha256', base64DecodedSecret); const computed_signature = hmac.update(signed_content).digest(); ``` This generated signature should match one of the ones sent in the `Webhook-Signature` header. The `Webhook-Signature` header is composed of a list of space delimited signatures and their corresponding version identifiers. The signature list is most commonly of length one. Though there could be any number of signatures. For example: ``` v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo= ``` Make sure to remove the version prefix and delimiter (e.g. v1,) before verifying the signature. Please note that to compare the signatures it's recommended to use a constant-time comparison method in order to prevent timing attacks. To check that your implementation is correct (except for timestamp verification), you can test against this sample data which is verified: ``` payload = '{"event_type": "TRANSFER_PROCESSED", "transfer_id": "dptx_ckyypz30n000101kgzgnrtqlf", "company_id": "cuacc_ckqckhadg000601r95ox48c2s"}' secret = "4j7OxQ4wlv1GmkZ9qLjoFjEFXjpzvHkr" webhook_id = "msg_24Ky2257Hzd0tgc5bWs8TwK9Kod" webhook_timestamp = "1643393361" webhook_signature = "v1,6mFFi/Bg0gw1Yz2KJwZSVq6Bh+XzllS7JVltAlZ8yCU= v1,9dEEi/Bg0gw1Yz2KJwZSVq6Bh+XzllS7JVltAlZ8yDY=" ``` #### Verify timestamp As mentioned above, Brex also sends the timestamp of the attempt in the `Webhook-Timestamp` header. You should compare this timestamp against your system timestamp and make sure it's within your tolerance in order to prevent timestamp attacks. ## For partners: Using the right access token For partners, in order to perform your business logic, you will likely need to take the payload of the webhook and make additional calls to the Brex API. In order to do so, if you have multiple clients, you should maintain a mapping of `company_id` to `access_token` as `company_id` will be passed along webhook payloads that are associated with a single copmany. The [companies endpoint in the Team API](/openapi/team_api/#tag/Companies) can help you build this mapping by giving you the reverse association of `company_id` from `access_token`. ## Code samples ### Verifying signature (NodeJS Example) ```javascript const https = require("https"); const crypto = require("crypto"); function verifySignature(request) { // grab the headers const webhook_id = request.get("Webhook-Id"); const webhook_signature = request.get("Webhook-Signature"); const webhook_timestamp = request.get("Webhook-Timestamp"); const body = request.body; verifyTimestamp(webhook_timestamp); const signed_content = `${webhook_id}.${webhook_timestamp}.${body}`; // Get signing secrets from Brex API const options = { hostname: "api.brex.com", path: "/v1/webhooks/secrets", method: "GET" }; return https.request(options, resp => { // Get array of secrets const secrets = resp.map(secretObj => secretObj.secret); // Split the signature string by the space delimiter, remove version and comma, map to array const passed_signatures = webhook_signature.split(" ").map(sigString => sigString.split(",")[1]); // iterate over each secret (usually there is only one, but there may be two during key rotation) // if any match our signed signature, we've verified the payload return secrets.some(secret => { // Compute the signature const base64DecodedSecret = Buffer.from(secret, 'base64'); const hmac = crypto.createHmac('sha256', base64DecodedSecret); const computed_signature = hmac.update(signed_content).digest(); // see if any of the signatures from the payload match our computed signature // using a timing safe comparison return passed_signatures.some(passed_signature => { const decodedPassedSignature = Buffer.from(passed_signature, 'base64'); return crypto.timingSafeEqual(computed_signature, decodedPassedSignature); }); }); }); }); const WEBHOOK_TOLERANCE_IN_SECONDS = 60; function verifyTimestamp(timestampString) { const now = Math.floor(Date.now() / 1000); const timestamp = parseInt(timestampString, 10); if (isNaN(timestamp)) { throw new Error("Invalid Signature Headers"); } if (now - timestamp > WEBHOOK_TOLERANCE_IN_SECONDS) { throw new Error("Message timestamp too old"); } if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) { throw new Error("Message timestamp too new"); } return; } ``` ### Webhook endpoint processing a failed transfer ```javascript function webhookProcessingEndpoint(request) { verifySignature(request).then(isVerified => { if (!isVerified) { throw new Error("Webhook Verification Failed"); } const payload = request.payload; const eventType = payload.event_type; switch (eventType) { case "TRANSFER_FAILED": const {transfer_id, company_id} = payload; // This logic can use the Transactions or Payments API to // get more info about the transfer/company by id. // Partners will need to map the company_id to the // appropriate `access_token` myAppsBusinessLogic(transfer_id, company_id); default: throw new Error("Unknown event type"); } }); } ``` ## IP whitelisting IP whitelisting allows you to restrict network access to specific IP addresses to ensure traffic from Brex is not compromised. Webhook events from Brex will use the following IP addresses. You can add these IP addresses to your application/firewall allowlist. Please add the full IP address list to ensure good coverage. ```sh 44.228.126.217 50.112.21.217 52.24.126.164 54.148.139.208 2600:1f24:64:8000::/52 ```