Webhooks
Webhooks allow you to receive HTTP POST requests with payloads whenever certain events happen. You can use the Webhook endpoints to add new or modify existing webhook configurations for your organization. The endpoints allow you to specify at which HTTPS URL you want to receive your webhook requests.
Events
Webhooks payloads will contain the new state of the resource as per after the event occurred. There are three types of events: CREATED
UPDATED
and DELETED
. If one wants to keep a copy of the data the Atlar platform holds, one need to listen to all three event types for a given resource and act accordingly.
Note: When creating a webhook one can provide an inclusive filter
to specify which event types one want to receive.
The event type
DELETED
may occur should data cleanup be necessaryThe event type
DELETED
may in some situations not be an event that can occur in a regular business process. For instance, for resourcetransactions
there is no business process that ever would remove resources as the resource is a booked entry on the bank statement.However, in some cases, maybe because of an issue on either Bank or Atlar side, there may be need for data cleanup, and in that case
DELETED
event will be sent.
Webhook payload
Below, the parts of the webhook payload is explained, but the technical details can be seen in API Reference > Events (webhooks).
- Metadata about which resource is affected and what API version the webhook carries (
resource
andapiVersion
respectively). - The resource, in its state as per after the event, that was affected by the event (
entity
),- Important to note that this might not technically be the absolute latest state. There might be situations where two events occur for the same resource in quick succession. It might then be that when first webhooks fires, the second event has already modified the resource in database and therefore first webhook carrying outdated information. This would of course momentarily after be corrected by the second webhook.
- Adding on delays and retries to above scenario, and there may be situation where the second webhook actually would hit your servers before the first webhook. The way to handle this as a webhook receiver is to note the
entity.version
, an incrementing integer and not store data where version is lower than the held version.
- Information about the event that occurred (
event
andresource
).
Webhook retries
Webhook requests are retried should they fail or if your server responds with anything but 2xx
HTTP statuses.
These systematic retries will happen 3 times every 15 minutes during 12 hours. If your system is down for more than 12 hours, or you lost your webhook information in some other way, you can retrieve the lost information on the event endpoints on the API to get all historic events for a specific resource.
Webhook security
We will send a few HTTP headers with each webhook request. Below is a summary of what headers we send and why.
- Signature
- Header:
Webhook-Signature
(case-insensitive) - Format: HMAC-SHA256
- Example:
e71354d023f850abcb1bfd884de2874bc973dcbb4b44acac5fe4ca89e28d12ed
(multiple signatures will be separated by a comma,
) - Purpose: Makes it possible for your app to verify that the webhook was sent from Atlar (authenticity), and that the message hasn't been altered (integrity).
- Header:
- Timestamp
- Header:
Webhook-Request-Timestamp
(case-insensitive) - Format: RFC 3339 (UTC, nano)
- Example:
2022-10-11T10:13:14.000000015Z
- Purpose: Used to mitigate replay attacks. The value is set to the time that the HTTP request was made.
- Header:
Webhook signature and timestamp verification
We will generate a unique webhook key for each and one of your endpoints, which you will see once after creating it via the Webhook API. Store the key as a secret in your application. We use this key to create the HMAC-SHA256 signature, and you can use it to validate our signature.
In your application, write a function that executes the following steps:
- Decode the key into bytes. The key is a byte array with standard base64 encoding.
- Create the signature payload by concatenating:
- The webhook request body as a JSON string.
- A period
.
. - The timestamp from the header (as a string).
- Compute a HMAC (Hash-based Message Authentication Code) using the SHA256 hash function.
- Encode the HMAC bytes into a hexadecimal string so it can be compared with the value in the signature header.
- Compare the two values using a constant-time string comparison (not a plain
==
). This helps to mitigate timing attacks. The HMAC library you're using might have an equality function for this purpose. - Compare the time difference between the request timestamp and the current time (UTC) and check that the message isn't too old. You can choose a certain time tolerance such as 5min. This helps to prevent replay attacks, where an attacker would re-send an old request.
Note that if you have multiple keys for one endpoint (during key rotation), there will be multiple signatures separated by a comma ,
.
Code example (Go)
func signatureIsValid(sigHeader, tsHeader, payload, base64Key string) (bool, error) {
key, err := base64.StdEncoding.DecodeString(base64Key)
if err != nil {
return false, fmt.Errorf("failed to decode key, %w", err)
}
sig, err := hex.DecodeString(sigHeader)
if err != nil {
return false, nil
}
mac := hmac.New(sha256.New, key)
mac.Write([]byte(payload + "." + tsHeader))
return hmac.Equal(mac.Sum(nil), sig), nil
}
(More examples in various languages (Go, Java, JavaScript, Python) are available at https://github.com/atlar-tech/atlar-webhook-examples)
Example values
Webhook-Signature: fe8f799f90ecfe57ce9ae19d3429be0ca3c0e5ae336fdf3e08dd1f7b60a15a6f
Webhook-Request-Timestamp: 2022-10-06T07:26:57.237369365Z
Key (base64): agj+xWKk3gqkP+SsCsljkjbDth7bxguqVMRd4K3wm1I=
Body: {"resource":"payments","event":{"organizationId":"1f91e001-9295-46b6-9438-ef6f0fed18fc","entityId":"422a164c-4548-11ed-8d31-0a58a9feac02","id":0,"timestamp":"2022-10-06T07:26:56.837022728Z","name":"CREATED","originator":"90b51164-d782-4238-a0bf-c1ff66a8a83e","message":"","details":{"request":"ewogICAgImFtb3VudCI6IHsKICAgICAgICAiY3VycmVuY3kiOiAiU0VLIiwKICAgICAgICAidmFsdWUiOiA1MDAwCiAgICB9LAogICAgInNvdXJjZUFjY291bnRJZCI6ICJjZjAxMGY5MC04M2UyLTQyMGQtYjE2ZS1lYjY4ZGJmZDcwNDciLAogICAgImRlc3RpbmF0aW9uRXh0ZXJuYWxBY2NvdW50SWQiOiAiZTgwZGU1ZTAtMWIwNS00OTc0LThjNzMtMDg0MDI3MTg1YzdmIiwKICAgICJkYXRlIjogIjIwMjItMTAtMTAiLAogICAgInJlbWl0dGFuY2VJbmZvcm1hdGlvbiI6IHsKICAgICAgICAidHlwZSI6ICJVTlNUUlVDVFVSRUQiLAogICAgICAgICJ2YWx1ZSI6ICJUZXN0aW5nIHdlYmhvb2tzIgogICAgfSwKICAgICJwYXltZW50U2NoZW1lVHlwZSI6ICJTQ1QiCn0="}},"entity":{"amount":{"currency":"SEK","value":5000},"approvalChain":{"approvalSteps":[{"id":"b698ab4e-efec-4c97-9133-88904022cd1b","requiredRole":{"id":"4ce82d1a-0ebf-46e7-96ba-7664cd48e4af","name":"Owner","owner":true},"status":"PENDING","updated":"0001-01-01T00:00:00Z"}],"triggeredApprovalChainId":"54e2c37d-fa95-4d5b-bbcf-b99c8ff8676f"},"attachedTransactions":[],"created":"2022-10-06T07:26:56.836974Z","date":"2022-10-10","destinationCounterparty":{"id":"7af0ea0b-7366-42aa-ad83-49e93e41ce5d","identifiers":[],"name":"Company Inc.","partyType":""},"destinationExternalAccount":{"bank":{"bic":"12345678900","id":"","name":""},"counterpartyId":"7af0ea0b-7366-42aa-ad83-49e93e41ce5d","id":"e80de5e0-1b05-4974-8c73-084027185c7f","identifiers":[{"holderName":"Test","market":"DE","number":"DE66500105172794778236","type":"IBAN"}],"organizationId":"1f91e001-9295-46b6-9438-ef6f0fed18fc"},"externalMetadata":null,"id":"422a164c-4548-11ed-8d31-0a58a9feac02","organizationId":"1f91e001-9295-46b6-9438-ef6f0fed18fc","paymentScheme":{"displayName":"Sepa Credit Transfer","type":"SCT"},"paymentSchemeType":"SCT","reconciliation":{"status":""},"remittanceInformation":{"type":"UNSTRUCTURED","value":"Testing webhooks"},"sourceAccount":{"affiliation":{"id":"49b72264-fae5-4720-89cf-f0d6fd5f727e","name":"My bank"},"bank":{"bic":"ATLRSESSXXX","id":"3dde93bf-49d1-44e4-a288-4bf0d4e31574","name":"Testbank"},"id":"cf010f90-83e2-420d-b16e-eb68dbfd7047","identifiers":[{"holderName":"Test Testsson","market":"DE","number":"DE77500105179251553356","type":"IBAN"}],"name":"My EUR account","owner":{"name":""}},"status":"CREATED"}}
Webhook idempotency and ordering of events
It's important that your webhook handler is idempotent. This means that two identical webhook calls will give the same result as one single call. Due to our behavior with retries, we can't guarantee that we won't send a webhook twice. Additionally, an attacker could resend a webhook in a replay attack. One way of achieving idempotency is to rely on a combination of the event.id
(example: 3
) and the entity.id
(example: e4588d47-8d7e-48a0-a6f5-6960a19b45b3
). These IDs will stay the same between retries, and combined they are unique for the given event that occurred. You can extract the IDs from the webhook payload and check if you have processed it before by storing previously seen IDs somewhere. Atlar will never send webhooks that are older than 120 hours, meaning you won't have to store the IDs for longer than that.
In addition to idempotency, it's important that your handler isn't strictly dependent on the ordering of the webhook calls. In most cases, webhooks will naturally be delivered in order, but this is no guarantee. To know the end state of the entity for the webhook, you can make use of the event.timestamp
and entity
fields. The entity
always contains the entity as it looked like right after an event occurred. For example, if an event occurred that resulted in a status change of a payment, the entity
will contain the updated payment with the new status.
Webhook key rotation
Webhook keys can be rotated as many times as you want. If your webhook key is compromised, you can use the rotation functionality to generate a new one. Use the /v1/webhooks/{id}/keys
endpoints to generate or delete keys. Since you can have between 1 and 2 keys at once, rotation can happen without any application downtime.
Going live with webhooks
Before you can start receiving webhook calls from Atlar, the webhook configuration must be verified
. You can contact us at [email protected] for this purpose. Note that modifying the url
of a webhook will set verified
to false.
Updated about 1 month ago