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 requestdata.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 ofsuccess
,failed
,expired
,canceled
, orpending_review
forflow_session.status.updated
events) indicating whether the user completed or failed the flow sessiondata.customer_reference
- The value you provided tocustomerReference
in your Javascript integration. You can use this to lookup the user this event is reporting aboutenvironment
- 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:
flow_session.step.updated
The user has started the Flow session and is on the first stepflow_session.step.updated
flow_session.status.updated
The user has reached a terminal state for their sessionflow_session.retried
A retry has been requested for this user, either via the dashboard or via APIflow_session.step.updated
flow_session.step.updated
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 aflow_session.status.updated
event. - A
flow_session.step.updated
event being delivered before an associatedflow_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 isflow_session.step.updated
- The
status
is marked asactive
- 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 Flowverify_sms
- The user is verifying their phone numberkyc_check
- The user is performing a Lightning Verificationdocumentary_verification
- The user is performing a Documentary Verificationselfie_check
- The user is recording a selfie video which is compared against their document if applicablescreening
- 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 isflow_session.retried
- The
status
is marked asactive
- 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:
Key | Value |
---|---|
API Secret | live_secret_abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234 |
API Key | live_key_deadbeefcafedeadbeefcafedeadbeef |
Customer Reference | cafecafe-beef-beef-beef-cafecafecafe |
Webhook Endpoint | https://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:
- The HTTP request method in lowercase (
post
) - The URI path of the request (
/webhook_receivers/flow
) - The value of the Date header
- 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:
anddigest:
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
andDigest
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.