Webhooks

Webhooks can notify your service about events that happen in an inquiry. See the Events page for a description of supported events.

Webhooks setup

To set up Webhooks, go to Configuration > Webhooks in the Dashboard.

Only enabled events in the specified environment will be sent to your application. You can use a tool like ngrok to test webhooks locally in your sandbox environment.

Responding to events

Webhook events are sent via POST according to the JSONAPI specification.

Affected and related objects are included in the payload attribute. Objects within the payload use the same schema as their API resource endpoints.

Your endpoint must return an 200 OK. If Persona does not receive an 200 OK from your application, it will retry until it is received, up to a maximum of 8 times.

📘

Including PII in the payload

Due to data privacy requirements and the sensitivity of the data, we do not expose the PII collected through the verification process by default. Based on your needs and how you plan to use the PII, we allow selected attributes (e.g. name) to be accessed via API.

🚧

Order of received events

The order of your received webhook events may not match the order of our events creation because of various factors like network request latency and exponential retries on failures. Included in the attributes of the request body is the event's created-at timestamp if you need to determine server-side ordering.

Retry Logic

Persona attempts to deliver your webhooks for up to 8 times with exponential backoff (3, 66, 731, 4098, 15627, 46658, 117651, 262146 seconds).

Handling duplicate events

Your webhook endpoints may occasionally receive the same event more than once. This is due to the nature of network connectivity. We recommend making your event processing idempotent to handle duplicate events. One way of doing this is to log events that you've processed and to skip processing for already-logged events.

Checking signatures

Each webhook event contains a Persona-Signature header with an HMAC. You can ensure that any request is authentic and safe to process by comparing this value with your own digest, computed from the request body and your webhook secret. Your webhook secret can be found in the Webhooks section of the Dashboard.

Example request from webhook:

{
  "data": {
    "type": "event",
    "id": "evt_XGuYWp7WuDzNxie5z16s7sGJ",
    "attributes": {
      "name": "inquiry.approved",
      "payload": {
        "data": {
          "type": "inquiry"
          "id": "inq_2CVZ4HyVg7qaboXz2PUHknAn",
          "attributes": {
            "status": "approved",
            "reference-id": null,
            "created-at": "2019-09-09T22:40:56.000Z",
            "completed-at": "2019-09-09T22:44:51.000Z",
            "expired-at": null
          },
          "relationships": {
            "reports": {
              "data": []
            },
            "template": {
              "data": {
                "id": "blu_biqYXr3aNfHuLeXUdJUNFNET",
                "type": "template"
              }
            },
            "verifications": {
              "data": [
                {
                  "id": "ver_KnqQRXmxmtquRE65CHTzymhR",
                  "type": "verification/driver-license"
                },
                {
                  "id": "ver_2aguhcwq66zcmqipmrqjxw68",
                  "type": "verification/selfie"
                }
              ]
            }
          },
        },
        "included": [
          {
            "type": "template"
            "id": "blu_biqYXr3aNfHuLeXUdJUNFNET",
            "attributes": {
              "name": "Acme #1"
            },
          },
          {
            "type": "verification/driver-license"
            "id": "ver_KnqQRXmxmtquRE65CHTzymhR",
            "attributes": {
              "status": "passed",
              "front-photo-url": "...",
              "created-at": "2019-09-09T22:41:29.000Z",
              "completed-at": "2019-09-09T22:41:40.000Z",

              ...
            },
          },
          {
            "type": "verification/selfie"
            "id": "ver_2aguhcwq66zcmqipmrqjxw68",
            "attributes": {
              "status": "passed",
              "center-photo-url": "...",
              "left-photo-url": "...",
              "right-photo-url": "...",
              "created-at": "2019-09-09T22:42:43.000Z",
              "completed-at": "2019-09-09T22:42:46.000Z",

              ...
            },
          }
        ]
      }
      "created-at": "2019-09-09T22:46:27.598Z",
    }
  }
}

The Persona-Signature header contains a timestamp t and a HMAC-SHA256 digest v1. The digest is computed from your webhook secret and a dot-separated string composed of the timestamp joined with the request body.

Sample code for verifying signatures:

t, v1 = request.headers['Persona-Signature'].split(',').map { |value| value.split('=').second }
computed_digest = OpenSSL::HMAC.hexdigest('SHA256', secret, "#{t}.#{request.body.read}")

if v1 == computed_digest
  # Handle verified webhook event
end
t, v1 = [value.split('=')[1] for value in request.headers['Persona-Signature'].split(',')]
computed_digest = hmac.new(secret.encode(), (t + '.' + request.body).encode(), 'sha256').hexdigest()

if hmac.compare_digest(v1, computed_digest):
  # Handle verified webhook event
const sigParams = {}
request.headers['persona-signature']
	.split(',')
	.forEach(pair => {
		const [key, value] = pair.split('=');
		sigParams[key] = value;
	})

if (sigParams.t && sigParams.v1) {
	const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET)
  	.update(`${sigParams.t}.${request.body}`)
  	.digest('hex');
  
	if (crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(sigParams.v1))) {
		// Handle verified webhook event
	}
}

🚧

Parsing JSON when Computing HMACs

In some languages, parsing the JSON may result in something that's not equivalent to the event body. For example, JavaScript may round floats and reduce precision. We recommend using the raw event body when computing the HMAC.

IP addresses

All webhook requests will come from the following IPs.

  • 35.232.44.140
  • 34.69.131.123
  • 34.67.4.225