Callback

Quickpay offers a callback service that will notify your system when you create, change or delete a resource. This is useful e.g. when you want to mark an order as paid, update your shops inventory and so on.

The callback service is asynchronous and as such will not interfer with or prolong the processing time of the API request generating a callback - eg. the time your customer will have to wait for payment confirmation.

In the event that your system is not able to receive or correctly process the callback, the callback service will try to deliver its message up to 24 times, with gradually increasing delays between each try.

Important! Currently only payment and subscription operations (authorize, capture etc.) will trigger a callback.

How it works

When you create, change or delete a resource a HTTP POST request is sent to the callback URL defined in your Account settings.

A callback URL can also be specified in each operation request to the API with the header QuickPay-Callback-Url, or in each request to the Payment Window with the field callbackurl.

Request headers

The request headers contains some important information.

Header Description
QuickPay-Resource-Type The type of resource that was created, changed or deleted
QuickPay-Account-ID The account id of the resource owner - useful if receiving callbacks for several systems on the same url
QuickPay-API-Version API version of the callback-generating request
QuickPay-Checksum-Sha256 Checksum of the entire raw callback request body - using HMAC with SHA256 as the cryptographic hash function. The checksum is signed using the Account's private key. We strongly recommend that you validate the checksum to ensure that the request is authentic.

Request body

The request body contains the resource as it exists after the change - ie. it is equivalent to GET /<resource>/<identifier>.

Response

We expect a response http status code of 2xx, 302 or 303 to the callback. Otherwise the callback is failed and will be retried after an hour. When http status in the response is 301 or 307 we redirect the callback to the url in the Location header.

Order of callbacks

The callbacks are asynchronous and can be delivered in any order. However, callbacks on the same resource are guaranteed to be delivered in the same order the operations (create, change or delete) happen. E.g. callbacks for authorize and capture on the same Payment will always be delivered in that order, but two callbacks on different Payments will come in any order.

Request example

This example shows a callback generated when authorizing a payment:

{
    "id": 110376903,
    "merchant_id": 5,
    "order_id": "14192826166",
    "accepted": true,
    "type": "Payment",
    "text_on_statement": null,
    "branding_id": null,
    "variables": {},
    "currency": "DKK",
    "state": "new",
    "metadata": {
        "type": "card",
        "origin": "form",
        "brand": "dankort",
        "bin": "100002",
        "last4": "0006",
        "exp_month": 8,
        "exp_year": 2022,
        "country": "DNK",
        "is_3d_secure": false,
        "issued_to": null,
        "hash": "d345e9e91c4b46f938662OWBm2Ucn2YCzoTby8bMmL2dLFlhInxa",
        "number": null,
        "customer_ip": "90.185.63.248",
        "customer_country": "DK",
        "fraud_suspected": false,
        "fraud_remarks": [],
        "fraud_reported": false,
        "fraud_report_description": null,
        "fraud_reported_at": null,
        "nin_number": null,
        "nin_country_code": null,
        "nin_gender": null,
        "shopsystem_name": null,
        "shopsystem_version": null
    },
    "link": {
        "url": "https://payment.quickpay.net/payments/ef45d3ca56c65733e777c4fe0cf0484a536d7329d20f1b9d79c16dab62b80195",
        "agreement_id": 11,
        "language": "da",
        "amount": 100,
        "continue_url": null,
        "cancel_url": null,
        "callback_url": "http://domain.com/callbackurl",
        "payment_methods": "",
        "auto_fee": false,
        "auto_capture": null,
        "branding_id": null,
        "google_analytics_client_id": null,
        "google_analytics_tracking_id": null,
        "version": "v10",
        "acquirer": null,
        "deadline": null,
        "framed": true,
        "branding_config": {},
        "invoice_address_selection": null,
        "shipping_address_selection": null,
        "customer_email": null
    },
    "shipping_address": null,
    "invoice_address": null,
    "basket": [],
    "shipping": null,
    "operations": [
        {
            "id": 1,
            "type": "authorize",
            "amount": 100,
            "pending": false,
            "qp_status_code": "20000",
            "qp_status_msg": "Approved",
            "aq_status_code": "000",
            "aq_status_msg": "Approved",
            "data": {},
            "callback_url": "http://domain.com/callbackurl",
            "callback_success": true,
            "callback_response_code": "200",
            "callback_duration": 121,
            "acquirer": "nets",
            "callback_at": "2018-03-20T08:48:36+00:00",
            "created_at": "2018-03-20T08:48:35Z"
        }
    ],
    "test_mode": true,
    "acquirer": "nets",
    "facilitator": null,
    "created_at": "2018-03-20T08:48:27Z",
    "updated_at": "2018-03-20T08:48:36Z",
    "retented_at": null,
    "balance": 0,
    "fee": null,
    "deadline_at": null
}

Checksum validation examples

These examples show how you can authenticate the callback request sent to your server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require 'openssl'

def sign(base, private_key)
  OpenSSL::HMAC.hexdigest('sha256', private_key, base)
end

# Assuming that you are using Rack - https://github.com/rack/rack
request_body = env["rack.input"].read
checksum     = sign(request_body, "your_account_private_key")

if checksum == env["HTTP_QUICKPAY_CHECKSUM_SHA256"]
  # Request is authenticated
else
  # Request is NOT authenticated
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
function sign($base, $private_key) {
  return hash_hmac("sha256", $base, $private_key);
}

$request_body = file_get_contents("php://input");
$checksum     = sign($request_body, "your_account_private_key");

if ($checksum == $_SERVER["HTTP_QUICKPAY_CHECKSUM_SHA256"]) {
  // Request is authenticated
} else {
  // Request is NOT authenticated
}

?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python
# -*- coding: utf-8 -*-
import hashlib
import hmac
import sys
PY3 = sys.version_info[0] >= 3

def sign(base, private_key):
    if PY3:
        return hmac.new(bytes(private_key, 'utf-8'), bytes(base, 'utf-8'
                        ), hashlib.sha256).hexdigest()
    else:
        return hmac.new(private_key, base, hashlib.sha256).hexdigest()

# Assuming that you are using Werkzeug - http://werkzeug.pocoo.org/
request_body = environ['wsgi.input'].read()
checksum = sign(request_body, 'your_account_private_key')

if checksum == environ['HTTP_QUICKPAY_CHECKSUM_SHA256']:
  # Request is authenticated
else:
  # Request is Not authenticated
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import crypto from 'crypto';

// assuming that you are using body-parser - https://github.com/expressjs/body-parser 
const apiKey = 'private-key'; 
const checksum = req.headers['quickpay-checksum-sha256']; 
const body = req.body; 
const bodyAsString = JSON.stringify(body); 
const bodyHashed = crypto 
  .createHmac('sha256', apiKey) 
  .update(bodyAsString) 
  .digest('hex');

if (checksum === bodyHashed) { 
  // Request is authenticated
} else { 
  // Request is NOT authenticated
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
string checkSum = Request.Headers["QuickPay-Checksum-Sha256"];

var bytes = new byte[Request.InputStream.Length];
Request.InputStream.Read(bytes, 0, bytes.Length);
Request.InputStream.Position = 0;
string content = Encoding.UTF8.GetString(bytes);

string compute = sign(content, "your_account_private_key");

if (checkSum.Equals(compute))
{
  // Request is authenticated
}
else
{
  // Request is NOT authenticated
}

private string sign(string base, string api_key) {
  var e = Encoding.UTF8;

  var hmac = new HMACSHA256(e.GetBytes(api_key));
  byte[] b = hmac.ComputeHash(e.GetBytes(base));

  var s = new StringBuilder();
  for (int i = 0; i < b.Length; i++)
  {
    s.Append(b[i].ToString("x2"));
  }

  return s.ToString();
}