Callbacks¶
Callbacks allow you to receive notifications whenever the state on a transaction changes (or something significant happens to it).
To set up Callbacks for the transactions, specify a callback_url
in the Integration settings or send it in the payment request. Whenever something significant happens to the transaction that we think it needs notification of (typically a state change), we make an HTTP POST request to your callback_url
with the object_id
in the body of the request.
Note
Callbacks are asynchronous and not recommended for time-critical applications. It is very much possible and even likely that callbacks reach your application out-of-order and that they get duplicated. For time-critical applications, we advise using the API reconciliation to poll our system for updates.
Configure Callbacks¶
To configure your callbacks, go to Account settings → Integration → Callbacks and add a callback URL.
Callback Requests¶
We send to your callback_url
the HTTP request with the following characteristics:
- a POST request;
- with Request Body in JSON API format (the same as in the API);
- contains the object
type
andid
in the body of the request (not as URL-parameters).
{
"data": {
"type": "payment-invoices",
"id": "cpi_yv1RgJ2l8ty2AxIs",
"attributes": {
"serial_number": "yv1RgJ2l8ty2AxIs",
"status": "processed",
"resolution": "ok",
"moderation_required": false,
"amount": 22,
"payment_amount": 22,
"currency": "USD",
"service_currency": "USD",
"reference_id": "da1b0b9d-c249-4f6e-9949-2a2f2d4b1758",
"test_mode": true,
"fee": 0,
"deposit": 22,
"processed": 1592232071,
"processed_amount": 22,
"refunded_amount": null,
"processed_fee": 0,
"processed_deposit": 22,
"metadata": {
"merchant_url": "https://yu.test.this"
},
"flow_data": {
"action": "https://example.domain/hpp/cgi_Q0fYDFaHlqb5MCdl",
"method": "GET",
"params": [],
"metadata": {
"sid": "cgi_Q0fYDFaHlqb5MCdl",
"token": "eyJ0eXAiOiJKV1QiLRiGCg2O...nGE7E"
}
},
"flow": "hpp",
"payment_flow": "charge",
"created": 1592232050,
"updated": 1592232071,
"payload": {
"token": null,
"client_ip": "54.36.117.30",
"payment_card": {
"last": "0000",
"mask": "512381******0000",
"brand": "mastercard",
"first": "512381",
"holder": null,
"expiry_year": "33",
"issuer_name": "FIRST DATA CORPORATION",
"expiry_month": "11",
"issuer_country": "US"
}
},
"description": null,
"descriptor": null,
"callback_url": "https://test.doc.home",
"return_url": "https://example.com",
"return_urls": {
"fail": "https://example.com/sorry-page",
"pending": "https://example.com/wait-for-it-page",
"success": "https://example.com/thank-you-page"
},
"original_data": null,
"rrn": null,
"approval_code": null,
"reserved_amount": null,
"reserve_expires": null,
"unreserved": null,
"source": null,
"callback_logs": []
},
"relationships": {
"payment-service": {
"data": {
"type": "payment-services",
"id": "payment_card_usd_hpp"
}
},
"payment-method": {
"data": {
"type": "payment-methods",
"id": "payment_card"
}
},
"customer": {
"data": null
}
},
"links": {
"self": "/api/payment-invoices/cpi_yv1RgJ2l8ty2AxIs"
}
}
}
{
"data": {
"type": "payout-invoices",
"id": "cpoi_sIzOuMKJg98J22NC",
"attributes": {
"serial_number": "sIzOuMKJg98J22NC",
"status": "processed",
"resolution": "ok",
"amount": 100,
"payout_amount": 100,
"currency": "USD",
"service_currency": "USD",
"service_amount": 100,
"service_payout_amount": 100,
"reference_id": "45284707-d243-439e-8b41-d657322e693b",
"test_mode": true,
"description": null,
"fee": 0,
"writeoff": 100,
"exchange_rate": 1,
"processed": 1621335982,
"processed_amount": 100,
"processed_fee": 0,
"processed_writeoff": 100,
"metadata": [],
"created": 1621335974,
"updated": 1621335982,
"fields": {
"card_number": "512381******0000"
},
"callback_url": "https://test.doc.home",
"source": "merchant_dashboard",
"callback_logs": [],
"payouts": [
{
"id":"po_4a3Bgz6YR3cRjW6k",
"rrn":null,
"amount":72.5,
"currency":"USD",
"status":"processed"
},
{
"id":"po_3cR4z6kgYR6aj3BW",
"rrn":null,
"amount":27.5,
"currency":"USD",
"status":"processed"
}
]
},
"relationships": {
"payout-service": {
"data": {
"type": "payout-services",
"id": "payment_card_usd"
}
},
"payout-method": {
"data": {
"type": "payout-methods",
"id": "payment_card"
}
},
"customer": {
"data": null
}
},
"links": {
"self": "/api/payout-invoices/cpoi_sIzOuMKJg98J22NC"
}
}
}
The Callback request body contains the body of the related invoice transaction.
To check invoice statuses, please read:
Signature¶
Spoynt signs data using secret keys in the X-Signature
callback request header.
The X-Signature generation algorithm
<?php
$secret="yourPrivateKey";
$callbackData='{"data":{"type":"payment-invoices","id":"cpi_exampleID","attributes":{"serial_number":"exampleID","status":"processed","resolution":"ok","moderation_required":false,"amount":1000,"payment_amount":1000,"currency":"USD","service_currency":"USD","reference_id":"yourReferenceId","test_mode":true,"fee":38,"deposit":962,"processed":1647077297,"processed_amount":1000,"refunded_amount":null,"processed_fee":38,"processed_deposit":962,"metadata":[],"flow_data":{"action":"https:\/\/pay.example.com\/hpp\/cgi_exampleId","method":"GET","params":[],"metadata":{"sid":"cgi_exampleId","token":"bearerToken-for-H2H-api-auth"}},"flow":"hpp","payment_flow":"charge","created":1647077285,"updated":1647077297,"payload":{"token":"token-if-tokenize-true","auth_type":"card","client_ip":"192.168.0.1","payment_card":{"last":"0000","mask":"512381******0000","brand":null,"first":"512381","holder":"Test Customer","network":"mastercard","expiry_year":"99","issuer_name":"FIRST DATA CORPORATION","expiry_month":"09","issuer_country":"US"}},"description":null,"descriptor":null,"callback_url":"https:\/\/your.callback.url","return_url":null,"return_urls":{"fail":"https:\/\/your.fail-return.url","pending":"https:\/\/your.default-return.url","success":"https:\/\/your.success-return.url"},"original_data":null,"rrn":null,"approval_code":null,"reserved_amount":null,"reserve_expires":null,"unreserved":null,"source":"merchant_api","callback_logs":[],"charged_back_amount":null,"resolution_message":null,"hpp_url":"https:\/\/api.example.com\/hpp?cpi=cpi_exampleID"},"relationships":{"payment-service":{"data":{"type":"payment-services","id":"payment_card_usd_hpp"}},"payment-method":{"data":{"type":"payment-methods","id":"payment_card"}},"customer":{"data":{"type":"customers","id":"cus_exampleID"}},"active-payment":{"data":{"type":"payments","id":"pay_exampleID"}}},"links":{"self":"\/api\/payment-invoices\/cpi_exampleID"}},"included":[{"type":"customers","id":"cus_exampleID","attributes":{"reference_id":"example-customer-id","email":null,"name":null,"phone":null,"description":null,"created":1646833439,"metadata":[],"avatar":"https:\/\/www.gravatar.com\/avatar\/exampleAvatarID?s=20&d=mm&r=g","archived":false,"processing_options":{"payment_enabled":true,"payout_enabled":true},"address":{"street":null,"country":null,"region":null,"city":null,"full_address":null,"post_code":null}},"relationships":{"commerce-account":{"data":{"type":"commerce-accounts","id":"coma_exampleID"}}}}]}';
$signature = base64_encode(sha1($secret . $callbackData . $secret, true));
echo $signature;
- where:
$secret
is one of your secret (private) keys:test
orlive
,$callbackData
is raw JSON data,$signature
is a generation algorithm.
Note
To be sure you got data from Spoynt, you should generate a signature using the appropriate private key and raw JSON Сallback's data. The result is a base64 string that you should compare with Сallback's header signature. As an example:
B86Af35b/IfM0z0rGROHw5gVw14=
Timeouts¶
Spoynt uses 3 timeout types for Callbacks
- The Connection Timeout is the timeout for making an initial connection to the callback URL's HTTP server.
- Read Timeout: after the initial connection setting, still can be a possibility of an indefinite waiting period for reading data from the HTTP server.
- Total Callback Timeout: in addition to the above, Spoynt also checks the total execution time through the callback execution timeout.
The values for each timeout are as follows:
Timeout | For test connection | For live connection |
---|---|---|
Connection Timeout | 10,000 мs | 20,000 мs |
Read Timeout | 10,000 мs | 20,000 мs |
Execution Timeout | 20,000 мs | 60,000 мs |
Callback delivery and auto retries¶
The HTTP code received in the response to Callbacks defines system further actions.
Code | Definition | Further actions |
---|---|---|
200 | Successful delivery | No retry required, next scheduled delivery attempts cancelled |
429 | Too Many Requests, Callback is undelivered | The system forcibly stops delivery retries and cancels next scheduled delivery attempts. |
If the system obtains any other status code in the response, we assume the delivery of Callback failed. As default, the failed callback request is resent with an increasing delay between each attempt:
- the 1st retry 1 minute after the initial attempt,
- the 2nd retry 2 minutes after the 1st retry,
- the 3rd retry 3 minutes after the 2nd retry,
- the 4th retry 4 minutes after the 3rd retry,
- the 5th retry 5 minutes after the 4th retry,
- the 6th retry 6 minutes after the 5th retry,
- and so on, up to 100 attempts or up to receiving
200
or429
HTTP status code.
We can set up the delay between retries and the number of attempts
Contact our support team that we may find the most appropriate settings for your account.
Note
Also, you could resend a callback manually if you wish to sync your data immediately. Go to Transaction overview → Callbacks, select the Callback and use the button on the right side to resend it.
Duplicate handling¶
Due to callback retries, your app may receive the same callback more than once. Ensure idempotency of the callback by detecting such duplicates within your app.
Idempotence is the property of certain operations in mathematics and computer science, whereby they can be applied multiple times without changing the result beyond the initial application. In short, making multiple identical requests has the same effect as making a single request.
To control processing idempotency use by examining the id
parameter in callback request data since its value is unique to operation and thus identifies it.
It isn't a severe issue: if your app's actions are always idempotent, you don't need to worry much about concurrency.
Out-of-Order Delivery¶
Callbacks can also arrive out-of-order due to issues such as network delays or failures. However, you can determine the order of the events by examining the updated
attribute of the resource sent by the callback. updated
is a timestamp value that updated for every change made to the transaction.
Batching¶
We batch callbacks for the same object that are relatively close together. For example, if a payment goes from a state created
to pending
to processed
immediately, you only receive one callback, and when you look up the status of the payment, it will be processed
.
It prevents us from overwhelming your servers with HTTP requests, and it prevents your app from having to build complicated logic around subsequent status changes. Your app should only care about the latest transaction state.
IPs whitelist¶
To increase the security of the interaction between the Spoynt platform and your server, use the white list of IP addresses.