Webhook Notifications¶
Our webhooks inform you of asynchronous payment status updates and events that are not directly triggered by request from your side.
To process notifications, you need to:
Expose an endpoint on your server¶
To receive notifications you need a server with an endpoint that can receive a JSON HTTP POST request.
The request body will contain an array of events. Each event object will have an
event
attribute which will decide the format of the
additionalData
attribute.
{
"events": [
{
"event": "someEvent",
"id": "1",
"triggeredAt": "1970-01-01T00:00:00Z",
"additionalData": {}
}
]
}
A request header, X-HMAC-SHA256-Signature
, will also be present in the
request. This should be used to verify that the request was sent by Kronor and
that it hasn’t been tampered with. This signature is the result of applying the
HMAC Function to the unparsed request
body together with the hmacSecret
returned by Kronor when you configure
your webhook URL. (See Activate webhook notifications.)
Your server should acknowledge the notification by responding with HTTP 200 and
[accepted]
in the response body. The notification will be considered
unsuccessful if the server takes longer than 10 seconds to reply.
We recommend that you store the notifications in their raw format, acknowledge the request and then process the notifications. That way you don’t lose any data in case of an error, and you won’t lose data in the webhooks that you aren’t specifically listening for, allowing you to process them at a later point in time.
Here’s an example Node JS server that accepts webhook requests and verifies the HMAC header.
const crypto = require('crypto')
// npm i express
const express = require('express')
// npm i body-parser
const bodyParser = require('body-parser')
const app = express()
const port = 12345
const hmacSecret = '77fd4252-cba8-4612-aa06-f3dad513e3b9' // TODO: add your own secret here
// middleware necessary to be able to get the raw body from express
const rawBodySaver = (req, res, buf, encoding) => {
if (buf && buf.length) {
req.rawBody = buf.toString(encoding || 'utf8')
}
}
const options = {
verify: rawBodySaver
}
app.use(bodyParser.json(options))
app.post('/webhook', (req, res) => {
const hmacSignature = req.headers['x-hmac-sha256-signature']
const rawBody = req.rawBody
const computedSignature = crypto
.createHmac('SHA256', hmacSecret)
.update(rawBody)
.digest('hex')
if (hmacSignature !== computedSignature) {
res.status(401)
res.send('unexpected hmac signature')
return
}
// store the events in your database
console.log(req.body)
res.send('[accepted]')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
Activate webhook notifications¶
Note that subsequent mutations will not generate a new hmacSecret
, the
original secret will be returned again.
Understand the types of notifications¶
Each notification contains an event
attribute that specifies which type
of event triggered the notification.
Note that all money is denoted in minor units. I.e.
{
"amount": 1234,
"currency": "EUR"
}
means 12 Euro and 34 Cents.
invoiceUpdate¶
Triggers at various points during the invoice flow of a customer.
{
"events": [
{
"event": "invoiceUpdate",
"id": "1",
"triggeredAt": "1970-01-01T00:00:00Z",
"additionalData": {
"invoiceInfo": {
"purchaseId": "2",
"customerName": "Alan Turing",
"expectedAmount": 10000,
"expectedAmountFormatted": "100,00 kr",
"dueDate": "1970-01-01T00:00:00Z",
"dueDateFormatted": "1 Januari 1970",
"orderNumber": "b11511e0-048a-481f-b61d-9b8d6a91cbb6",
"invoicePageJwt": "eyJzZWNyZXRfbWVzc2FnZSI6ICJ3aHkgYXJlIHlvdSByZWFkaW5nIHRoaXMgeW91IG5lcmQ/In0=",
"reminderFeeAmount": "3000",
"reminderFeeAmountFormatted": "0,00 kr",
"currency": "SEK"
},
"invoiceEvent": "<INVOICE_EVENT>"
}
}
]
}
Where the possible values of <INVOICE_EVENT>
are:
invoiceCreated
invoiceSettled
invoiceDebtCollection
invoiceTransferringToAccount
invoiceReminder
invoiceSoftreminder
paymentStateUpdate¶
Triggers at various points during the payment flow of a customer.
{
"events": [
{
"event": "paymentStateUpdate",
"id": "1",
"triggeredAt": "1970-01-01T00:00:00Z",
"additionalData": {
"state": "<PAYMENT_STATE>",
"paymentType": "<PAYMENT_TYPE>",
"paymentInfo": {
"amount": 10000,
"amountPaid": 5000,
"amountAuthorized": 15000,
"amountToBePaid": 5000,
"currency": "SEK",
"reference": "b11511e0-048a-481f-b61d-9b8d6a91cbb6",
"reference2": "this is merchant reference 2"
}
}
}
]
}
Where the possible values of <PAYMENT_STATE>
are:
FLOW_COMPLETED
ACCEPTED
AUTHORIZED
PAID
CANCELLED
CAPTURE_DECLINED
where CAPTURE_DECLINED
is:
for payments that cannot be captured anymore as the card is declined from the payment gateway.
The actionable step here, for you as the merchant, is to contact the customer to pay the amountToBePaid
via other means.
where FLOW_COMPLETED
is:
for bank transfer payments, the user interaction has been completed and we are waiting for confirmation from the bank servers. The actionable step here, for the merchant, is to let the customer know the order has been accepted and that it’ll be confirmed once the funds are received.
where ACCEPTED
is:
for bank transfer payments, the funds have been debited from the customers bank account. The merchant should wait for PAID state before shipping the order.
And the possible values of <PAYMENT_TYPE>
are:
SWISH
BANK_TRANSFER
CREDIT_CARD
MOBILEPAY
VIPPS
captureStateUpdate¶
Triggers after a successful or failed payment capture.
{
"events": [
{
"event": "captureStateUpdate",
"id": "1",
"triggeredAt": "1970-01-01T00:00:00Z",
"additionalData": {
"state": "<CAPTURE_STATE>",
"paymentType": "<PAYMENT_TYPE>",
"captureInfo": {
"amount": 10000,
"captureId": "12345",
"currency": "SEK",
"paymentId": "987652"
}
}
}
]
}
Where the possible values of <CAPTURE_STATE>
are:
CAPTURE_DONE
ERROR
CAPTURE_DECLINED
CAPTURE_COOLDOWN
where CAPTURE_DECLINED
is:
for payments that cannot be captured anymore as the card is declined from the payment gateway. The actionable step here, for you as the merchant, is to contact the customer to pay via other means.
where CAPTURE_COOLDOWN
is:
In rare occasions the result of a capture operation can be a soft decline, with the possibility of recovering. A retry could be performed later but is not required as no money has been moved. Most notable cooldowns are for acquirer Nets and Forbrugsforeningen cards. Partial captures need to be 24 hours apart.
When retrying a new idempotency key must be used as it is a separate transaction.
And the possible values of <PAYMENT_TYPE>
are:
CREDIT_CARD
MOBILEPAY
VIPPS
refundStateUpdate¶
Triggers after an update occurs to a refund that was initiated.
{
"events": [
{
"event": "refundStateUpdate",
"id": "1",
"triggeredAt":"1970-01-01T00:00:00.000000+00:00",
"additionalData": {
"state":"<REFUND_STATE>",
"refundType":"<REFUND_TYPE>",
"refundInfo": {
"paymentId": "3980",
"refundId": "ade057da-3756-4419-a805-11d6728d526d",
"amount": 1000,
"currency": "SEK",
"errorMessage": "<REFUND_ERROR_MESSAGE>"
}
}
}
]
}
Where the possible values of <REFUND_STATE>
are:
PAID
ERROR
And the possible values of <REFUND_TYPE>
are:
SWISH
Where <REFUND_ERROR_MESSAGE>
is:
reason provided by the payment provider for the failure of the refund.
null
otherwise.
purchaseInstalmentFirstReminder¶
Triggered when three days have passed since an instalment payment was due and the expected amount hasn’t been paid.
{
"events": [
{
"event": "purchaseInstalmentFirstReminder",
"id": "1",
"triggeredAt": "1970-01-01T00:00:00Z",
"additionalData": {
"instalment": {
"dueDate": "1970-01-01T00:00:00Z",
"amountDue": 13000,
"amountPaid": 0,
"serviceFee": 0,
"reminderFee": 3000,
"currency": "SEK",
"partNumber": 1,
"paymentMethod": "SWISH"
},
"purchase": {
"id": "2",
"merchantReference": "b11511e0-048a-481f-b61d-9b8d6a91cbb6",
"createdAt": "1970-01-01T00:00:00Z"
},
"customer": {
"id": "3",
"firstName": "Haskell",
"lastName": "Curry",
"email": "[email protected]"
}
}
}
]
}
purchaseInstalmentFinalReminder¶
Triggered when ten days have passed since an instalment payment was due and the expected amount hasn’t been paid.
{
"events": [
{
"event": "purchaseInstalmentFinalReminder",
"id": "1",
"triggeredAt": "1970-01-01T00:00:00Z",
"additionalData": {
"instalment": {
"dueDate": "1970-01-01T00:00:00Z",
"amountDue": 16000,
"amountPaid": 0,
"serviceFee": 0,
"reminderFee": 6000,
"currency": "SEK",
"partNumber": 1,
"paymentMethod": "SWISH"
},
"purchase": {
"id": "2",
"merchantReference": "b11511e0-048a-481f-b61d-9b8d6a91cbb6",
"createdAt": "1970-01-01T00:00:00Z"
},
"customer": {
"id": "3",
"firstName": "Alonzo",
"lastName": "Church",
"email": "[email protected]"
}
}
}
]
}
purchaseStateUpdate¶
Triggers at various points during the purchase flow of a customer.
{
"events": [
{
"event": "purchaseStateUpdate",
"id": "1",
"triggeredAt": "1970-01-01T00:00:00Z",
"additionalData": {
"merchantReference": "b11511e0-048a-481f-b61d-9b8d6a91cbb6",
"purchaseId": "2",
"state": "<NEW_STATE>"
}
}
]
}
Where the possible values of <NEW_STATE>
are:
WAITING_FOR_CAPTURE
CANCELLED
REJECTED
RETURNED
Queued notifications¶
The webhook notifications will be considered unsuccessful if the server receiving the webhook can’t be reached or does not reply with the expected acknowledgement within 10 seconds. In that case the undelivered notifications will be queued up and retried at a later time. The time between attempts will increase exponentially with subsequent failures according to the following schedule.
2 minutes
5 minutes
10 minutes
30 minutes
1 hours
2 hours
4 hours
8 hours (repeat here until 7 days have passed)
If the notifications have not been successfully delivered after seven days of trying then webhook notifications will be disabled. To re-enable the webhook notifications you need to Activate webhook notifications again.