Webhooks

Numeral uses webhooks to notify your application when an event is created. Events mainly relate to new Numeral objects being created or during objects' status changes. Webhooks are particularly helpful for asynchronous events, such as payment being executed by a bank or an account statement being received from a bank.

In order to use webhooks, you should create an endpoint on your server and register this endpoint when creating the webhook in Numeral's dashboard.

For each event, Numeral will then send a POST request to your endpoint in JSON format. This API call contains an event object. Learn more in the Events section.

To acknowledge receipt of an event, your endpoint must respond with a 2xx HTTP status code to Numeral within 5 seconds. If the endpoint takes longer to respond or returns an HTTP status code different from 2xx, the webhook will be re-sent at a later time using an exponential backoff strategy.

To guarantee that your webhook endpoint receives the events in the correct order:

  • The next event is sent once the previous event has been received
  • Events are delivered steadily over time one by one
  • The processing of events stops at the first failed event

Signature mechanism

All webhooks sent by Numeral are signed. The signature is included in the TX-Numeral-Signature-1 HTTP header of each webhook event. Each time Numeral rotates its public key, a new TX-Numeral-Signature-X HTTP header with an incremental version number will be provided to be retro-compatible until customers migrated their code.

You should verify this signature using the RSASSA-PKCS1-v1_5 signature verification, with the following public keys:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoPYyfIykF1MH1A1G1qme
5b9z4U0ALKdY69gH9rual3ZmhX2+8WtGRrI3ND2NOp/VPOsOHLq/81Vl8om+y0OZ
vHHhaCi7yx+A7VS6dB+iy+5Uo4ILh4Srx58oDL2lfhuZPc+BsgP1bP3KZp5OAV29
eZFjKPqi+yIbZyOf2HgmxawXrRfhCZf3GNYUP2Ihb9z0URYzpswezoog0ql1V7b1
TzspGflPfBp0kXTsqk8bRkGbAYPOAM7w9/GJ8X/IaGhgjrikrzYqp1srKXCqHruW
Dr9VKwoG49AzFEptSQ6lqt6T9kImPiUkF0r9Xcq5h1YBgXGYr9EQHWG8146tQ+nd
CQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnxkk9/CVep8z9ZDq55VA
6tdPe/ODF7/SjB/MaFlGgsKMfZxBAKGvjGtK5FJriAq4i+k8aTULYkojOGHVQIYT
di5qKx9xaRs+c45c2sPudWOlLEzs3aJG/jQNolZAQ4aSx+qYwT54v8LcN61IxOkc
5ZlLXGsPfkL96DdPOstjv0fp/SfyCTibREmE+JpcEVPiGkfw+f5JYOQEVThMDCa4
uN/sJKvDO/NhF6cXTv3Hb8C0yYLfIO/vkzkXomUuY+Va47liL1pwik87yHeyTAAe
QOuknnV2K/sXeBU14b/96CP2v0H8h27w11IokpJwzOIMCQnh5zpaBeXAEjr+yr4f
aQIDAQAB
-----END PUBLIC KEY-----

In addition to the signature header, each webhook event holds a TX-Numeral-Request-Timestamp HTTP header that contains the UTC Unix timestamp of the time the event was triggered.

Signature verification process

Numeral’s webhook signature verification process involves the following variables:

  • The request’s body
  • The request’s timestamp (TX-Numeral-Request-Timestamp HTTP header)
  • The request’s signature (TX-Numeral-Signature-X HTTP header, "X" being the latest version)

Verification steps:

  • Start by concatenating the request’s raw body with the dot . character, followed by the request’s timestamp ({request_body}.{request_timestamp}). Here’s an example:
{"id":"cf1ce879-f779-4b4b-bd9a-0747ef3dc18b","object":"event","topic":"file","type":"created","data":{"id":"6312697e-a11f-4f11-84cf-8e32a9cfc289","size":2605,"format":"pacs.008","object":"file","status":"created","summary":{},"category":"payment_file","filename":"211018231401AecA.1634551061.xml","bank_data":{"message_id":""},"direction":"outgoing","created_at":"2021-10-18T09:57:41.559011Z","status_details":"generated on 2021-10-18","connected_account":"22222222-0000-0000-0000-000000000001"},"status":"delivered","status_details":"","related_object_id":"6312697e-a11f-4f11-84cf-8e32a9cfc289","related_object_type":"file","created_at":"2021-10-18T09:57:41.586741Z"}.1666192986
  • Using the SHA 256 hashing function, produce the hash of the above concatenation
  • Base64 decode the request’s signature
  • Use the RSASSA-PKCS1-v1_5 algorithm to verify the signature using the produced hash, the decoded request’s signature and Numeral’s latest public key

Code example

Below is an example using Go as a language:

package main

import (
	"crypto"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"errors"
	"log"
)

var signature string = `Xt9B54lOqLCCkNrjLdSp1KuYKYO8zmm274koTNYtNjZEgWiGk3cHHod4KKSdYVt5OzrPNGz3HgJpc1cxUmLS11ng1IP7aXqM3pzTGJHycAUxbEqd4OhNNr/bjyScSAeiogesQmaBMWNcuUNa/7Up0isCmuySPlIV81jL6GRu9GXu88EeHwGaWd4Kzg7HMOciB48ueB3XLwUo9ez1WPoooJ9bfzDxrSfhPpAx9CoUuEH3aXYJpVuTUjtI8WnvhWuVIUGscUUzbAomEM+y9CImHDZP0QSEPfVYpWt/r8QcG/zukhvuWNSHtAPnqxaU8LOgdfsVSgQxBcMDlNgPCZn+cA==`
var publicKey string = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3KAvpLM4ng9ppG/Z3kQF
3fRWYUYpJ2Z2h+DIcGuXyP2Hn5PAxwHNTQj0nXzpmsOCO8C1TghKwfDaFcGCfURQ
t/o8E1LmS5/ckMWsQwxKNbiwLlrFZFo8opdAOA+OTORdqq6+J18YRTCJEMClKkvI
AsDmgFANWApLkYx+r9pE9Kdasu3MTvVs0DpQNPG1guFwXUoVEEYIX7nmZvfUdqgM
bo1NQRvmAVOwWz2HpQ6b2t478IKMX+PHRs9Tn00/owKtAAoGj470IERXMNIZqBQu
geo558phv+J2hmc+CWp4hgO9skeZD71iCA5rd8PdZmj+SU0u/1eyKfE9zAtVfj4H
awIDAQAB
-----END PUBLIC KEY-----`
var timestamp = "1666272169"
var body = "{webhook_body}"

func main() {
	numeralPublicKey, err := parseRSAPublicKeyFromPEM(publicKey)
	if err != nil {
		log.Fatal(err)
	}

	decodedSignature, err := base64.StdEncoding.DecodeString(signature)
	if err != nil {
		log.Fatal(err)
	}

	// Build the hash with the webhook's body, the . character and the timestamp.
	hashed := sha256.Sum256([]byte(body + "." + timestamp))

	// Use the RSA PKCS #1 v1.5 algorithm to verify the signature.
	err = rsa.VerifyPKCS1v15(numeralPublicKey, crypto.SHA256, hashed[:], decodedSignature)
	if err != nil {
		log.Fatal(err)
	}

	log.Println("Signature is OK.")
}

func parseRSAPublicKeyFromPEM(pemString string) (*rsa.PublicKey, error) {
	block, _ := pem.Decode([]byte(pemString))
	if block == nil {
		return nil, errors.New("failed to parse public key from PEM block")
	}

	pub, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, err
	}

	pkey, ok := pub.(*rsa.PublicKey)
	if !ok {
		return nil, errors.New("key is not of RSA type")
	}

	return pkey, nil
}

Webhook event idempotency

In order to guarantee that each event is unique, a unique ID is passed through in the TX-Webhook-ID HTTP header. An event that has been sent twice will be sent with the same header value.