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 REFERRAL_CREATED Triggered once a referral is created, whether by API or mobile/web
Onboarding REFERRAL_ACTIVATED Triggered when a referred contact signs up for Brex using your referral link
Onboarding REFERRAL_APPLICATION_STATUS_CHANGED Triggered when the product application status is changed
Payments TRANSFER_PROCESSED Triggered when a transfer is successfully received
Payments TRANSFER_FAILED Triggered when a transfer fails
Team USER_UPDATED Triggered when a user is updated

For more details on each event and their associated payloads, see the Webhook Events API Reference.

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 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 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:

Copy
Copied
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 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:

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
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 can help you build this mapping by giving you the reverse association of company_id from access_token.

Code samples

Verifying signature (NodeJS Example)

Copy
Copied
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: "platform.brexapis.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

Copy
Copied
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.

Copy
Copied
44.228.126.217
50.112.21.217
52.24.126.164
54.148.139.208
2600:1f24:64:8000::/52
Copyright © Brex 2019–2022. All rights reserved.