Webhooks

Subscribe to events and get real-time alerts for changes via webhook

Once you have Flow embedded in your app, you should be able to complete an entire Flow session and review it in our dashboard.

To complete your integration, you need to add a webhook receiver to your application. For example, if say your application's domain is fintechly.co. Your webhook can be any route on your application that is ready to receive webhooks. For this example, lets say our endpoint is /webhook_receivers/flow.

First, we need to visit the Flow dashboard, open the integration settings panel, and open the "Webhooks" panel, and click "Create new Webhook".

Our webhooks integration lets you select which events you want to subscribe to. For flow, we offer three events:

  • flow_session.step.updated
  • flow_session.status.updated
  • flow_session.retried

Let's start with the simplest possible integration. For our example, we would set the URL to https://fintechly.co/webhook_receivers/flow, select the flow_session.status.updated event, and save the webhook.

With this webhook configured, Cognito will now send our application an HTTP POST request every time a user completes their Flow session. An example POST request might look like this:

POST https://fintechly.co/webhook_receivers/flow HTTP/1.1
Date: Sat, 23 Jan 2021 21:43:14 GMT
Digest: SHA-256=xZI8wiAi5crBdZt7l10plN7Q8bScB6r/OV5PjxjKtTw=
Authorization: Signature keyId="live_key_deadbeefcafedeadbeefcafedeadbeef",algorithm="hmac-sha256",headers="(request-target) date digest",signature="PkvXq6CcH0d5HA7hiK5JWsA+e7G+7fuZPLtM2rMe4/8="
Content-Type: application/json
Accept: application/json
Cognito-Version: 2020-08-14
Content-Length: 340

{"id":"whkevt_11111111111111","timestamp":"2021-01-01T00:00:00Z","event":"flow_session.status.updated","data":{"object":"flow_session","id":"flwses_11111111111111","status":"failed","step":null,"customer_reference":"cafecafe-beef-beef-beef-cafecafecafe","_meta":"This API format is not v1.0 and is subject to change."},"environment":"live"}

Where the JSON body (when pretty printed) looks like this:

{
  "id": "whkevt_11111111111111",
  "timestamp": "2021-01-01T00:00:00Z",
  "event": "flow_session.status.updated",
  "data": {
    "object": "flow_session",
    "id": "flwses_11111111111111",
    "status": "failed",
    "step": null,
    "customer_reference": "cafecafe-beef-beef-beef-cafecafecafe",
    "_meta": "This API format is not v1.0 and is subject to change."
  },
  "environment": "live"
}

Breaking down this event a bit, we provide:

  • event - The event type, to help you handle the request
  • data.id - The session id linked to your customer. You can use this to look up your customer in our dashboard. You can use this id to retrieve the complete Flow session via our API.
  • data.status - An enum (one of success, failed, expired, canceled, or pending_review for flow_session.status.updated events) indicating whether the user completed or failed the flow session
  • data.customer_reference - The value you provided to customerReference in your Javascript integration. You can use this to lookup the user this event is reporting about
  • environment - An enum indicator (always either "sandbox" or "live") that tells you whether this webhook is from our test or live environment

Because Flow is also currently still in beta, we include the _meta tag as a reminder that we may change this API format before we hit v1.0.

Event ordering

Cognito does not guarantee that webhooks will be delivered in any particular order. For example, while the logical ordering of webhooks for a Flow session might look like this:

  1. flow_session.step.updated The user has started the Flow session and is on the first step
  2. flow_session.step.updated
  3. flow_session.status.updated The user has reached a terminal state for their session
  4. flow_session.retried A retry has been requested for this user, either via the dashboard or via API
  5. flow_session.step.updated
  6. flow_session.step.updated
  7. flow_session.status.updated The retry has been completed

you should be prepared to handle these events in any delivery order. For example, consider whether your application will properly handle:

  • A flow_session.step.updated event being delivered after a flow_session.status.updated event.
  • A flow_session.step.updated event being delivered before an associated flow_session.retried

In order to properly handle webhook events being delivered out of order, your application should lookup the customer's associated Flow session(s) via our API.

Securing your webhooks

We sign all of the webhooks we send you, and it's important for you to verify the signature so that an attacker cannot forge requests and trick your backend into updating verification information without your knowledge. To make integration easier, we provide reference implementations for most popular programming languages.

If we don't have a client library for your language, you can contact support@cognitohq.com and we'll happily help you with your implementation. You can also read our "Verifying Webhook Signatures" walkthrough below to see an example implementation that includes example inputs and outputs that you can use to test that your implementation is working.

Responding to webhooks

We expect that your server will respond to our webhooks with an HTTP status of 200. Anything besides a 200 will be treated as an error and Cognito will attempt to re-deliver each webhook event up to 7 times. To make sure we don't overload your server when it is in a failing state, retries will be reattempted with exponential backoff with the final retry executing approximately 4 days after the original attempt.

If your webhook endpoint exclusively responds with errors for 3 days in a row, we will send you a warning email letting you know that your webhook receiver seems to not be working. If it is still broken after 1 more day, we will disable the failing webhook. If this happens, you can turn the webhook receiver back on in your settings once you have fixed the issue.

Handling step events

Our example above explains how to process flow_session.status.updated events, for receiving information about completed Flow sessions. You can also subscribe to events about step changes but enabling flow_session.step.updated events. A step update's payload would look like this:

{
  "id": "whkevt_22222222222222",
  "timestamp": "2021-01-01T00:00:00Z",
  "event": "flow_session.step.updated",
  "data": {
    "object": "flow_session",
    "id": "flwses_11111111111111",
    "status": "active",
    "step": "kyc_check",
    "customer_reference": "cafecafe-beef-beef-beef-cafecafecafe",
    "_meta": "This API format is not v1.0 and is subject to change."
  },
  "environment": "live"
}

The main differences here are:

  • The event field is flow_session.step.updated
  • The status is marked as active
  • The step field is filled, indicating which step the user is currently completing

The step field can have the following values:

  • accept_tos - The user has started your Flow
  • verify_sms - The user is verifying their phone number
  • kyc_check - The user is performing a Lightning Verification
  • documentary_verification - The user is performing a Documentary Verification
  • selfie_check - The user is recording a selfie video which is compared against their document if applicable
  • screening - The user is being checked by our watchlist screening product (applies if you have linked a screening program)
  • risk_check - Risk fields are being computed for this user

Handling retry events

It is possible to request that a user retry part or all of a Flow session through our dashboard or through our API. Whenever a new Flow session is started via the retry system, we will deliver the flow_session.retried webhook event.

{
  "id": "whkevt_22222222222222",
  "timestamp": "2021-01-01T00:00:00Z",
  "event": "flow_session.retried",
  "data": {
    "object": "flow_session",
    "id": "flwses_11111111111111",
    "status": "active",
    "step": "accept_tos",
    "customer_reference": "cafecafe-beef-beef-beef-cafecafecafe",
    "_meta": "This API format is not v1.0 and is subject to change."
  },
  "environment": "live"
}

Additional details:

  • The event field is flow_session.retried
  • The status is marked as active
  • The step field is filled, indicating the first step to be completed in the new Flow session

Handling other events

We also deliver webhook events as part of our Screening product, which can be connected to Flow via a setting in your Flow template. To learn more about the screening.status.updated and entity_screening.status.updated webhooks, see the relevant documentation for updating Screening individiuals and entities.

Verifying Webhook Signatures

Before implementing your own webhook signature verification, check to see if your programming language is supported by our client libraries.

This example uses the following values:

KeyValue
API Secretlive_secret_abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234
API Keylive_key_deadbeefcafedeadbeefcafedeadbeef
Customer Referencecafecafe-beef-beef-beef-cafecafecafe
Webhook Endpointhttps://fintechly.co/webhook_receivers/flow

A valid inbound webhook for this configuration would look like this:

POST https://fintechly.co/webhook_receivers/flow HTTP/1.1
Date: Sat, 23 Jan 2021 21:43:14 GMT
Digest: SHA-256=xZI8wiAi5crBdZt7l10plN7Q8bScB6r/OV5PjxjKtTw=
Authorization: Signature keyId="live_key_deadbeefcafedeadbeefcafedeadbeef",algorithm="hmac-sha256",headers="(request-target) date digest",signature="PkvXq6CcH0d5HA7hiK5JWsA+e7G+7fuZPLtM2rMe4/8="
Content-Type: application/json
Accept: application/json
Cognito-Version: 2020-08-14
Content-Length: 340

{"id":"whkevt_11111111111111","timestamp":"2021-01-01T00:00:00Z","event":"flow_session.status.updated","data":{"object":"flow_session","id":"flwses_11111111111111","status":"failed","step":null,"customer_reference":"cafecafe-beef-beef-beef-cafecafecafe","_meta":"This API format is not v1.0 and is subject to change."},"environment":"live"}

To securely verify this webhook, we need to recompute the signature value in the authorization header. Let's walk through how we compute that signature:

First, let's start with the Digest header's value. This value is a Base64 encoded SHA256 digest of the request's body. Your Base64 implementation should follow the RFC 4648 Base 64 Encoding specification:

require 'base64'
require 'digest'

# Important! Make sure you are using the raw body from the inbound HTTP request.
# If you are using a framework that automatically deserializes incoming JSON bodies,
# re-serializing back to JSON might not produce the same output!
body = <<~JSON.chomp
{"id":"whkevt_11111111111111","timestamp":"2021-01-01T00:00:00Z","event":"flow_session.status.updated","data":{"object":"flow_session","id":"flwses_11111111111111","status":"failed","step":null,"customer_reference":"cafecafe-beef-beef-beef-cafecafecafe","_meta":"This API format is not v1.0 and is subject to change."},"environment":"live"}
JSON

digest_header = 'SHA-256=' + Base64.strict_encode64(Digest::SHA256.digest(body))

puts digest_header # => SHA-256=xZI8wiAi5crBdZt7l10plN7Q8bScB6r/OV5PjxjKtTw=

Next, we need to construct the string that we'll be signing. The signing string should look like this:

(request-target): post /webhook_receivers/flow
date: Sat, 23 Jan 2021 21:43:14 GMT
digest: SHA-256=xZI8wiAi5crBdZt7l10plN7Q8bScB6r/OV5PjxjKtTw=

This string is a construction of:

  1. The HTTP request method in lowercase (post)
  2. The URI path of the request (/webhook_receivers/flow)
  3. The value of the Date header
  4. The value of the digest header

For this example, the signing string should look exactly as it does above. As a sanity check, note:

  • There is no trailing new line
  • Each line is separated by a newline
  • The (request-target):, date: and digest: prefixes are in lowercase!
  • The HTTP method (post) is in lower case. This is a common source of bugs since most application frameworks will provide the HTTP method in uppercase.

Once we have our signing string, we compute a Base64 encoded SHA-256 HMAC digest just like we did in an earlier section, to compute the customer reference signature.

require 'base64'
require 'openssl'

signature = Base64.strict_encode64(
  OpenSSL::HMAC.digest(
    'SHA256',
    api_secret,
    signing_string
  )
)

puts signature # => PkvXq6CcH0d5HA7hiK5JWsA+e7G+7fuZPLtM2rMe4/8=

If you've implemented everything correctly, you should get PkvXq6CcH0d5HA7hiK5JWsA+e7G+7fuZPLtM2rMe4/8= which is the same value we see in the webhook's Authorization header!

Now that we've computed the signature, all we have to do is check that it matches the value in the Authorization request. To do this, we use our signature and API key to reconstruct the header value and do the comparison:

def webhook_signature_match?(headers:, key:, computed_signature:)
  expected_header = %(Signature keyId="#{key}",algorithm="hmac-sha256",headers="(request-target) date digest",signature="#{computed_signature}")

  # Compare the actual and expected header in constant time, to avoid timing attacks
  # @see https://en.wikipedia.org/wiki/Timing_attack
  # @see https://www.rubydoc.info/gems/rack/Rack%2FUtils.secure_compare
  return Rack::Utils.secure_compare(headers.fetch('Authorization'), expected_header)
end

puts webhook_signature_match?(
  headers: headers,
  key: 'live_key_deadbeefcafedeadbeefcafedeadbeef',
  computed_signature: signature
) # => true

Finally, as a last security measure, you should also make sure to check the Date header to confirm it is within 15 minutes of the current time on your server:

fifteen_minutes = (60 * 15)

(Time.httpdate(headers.fetch('Date')) - Time.now).abs < fifteen_minutes

That's it! If the Date header is within and acceptable range and the signature is valid, it's safe to process the request.

Rationale

We know this is a bit complicated, so here are a few reasons why our webhooks work this way:

  • Computing a cryptographic signature means that an attacker cannot forge new requests to your webhook endpoint, even if they have somehow intercepted past webhooks
  • Requiring that the Date header is within 15 minutes of your server time means that, even if an attacker intercepted a past webhook to your server, they only have a 15 minute window to try to resend the request to your server.
  • Signing the Date and Digest headers means that an attacker cannot alter the values of a webhook they intercepted in the past and submit it. Any change to these values will change the signature.

If you're curious to learn more, this behavior is based on this "Signing HTTP Messages" specification.