Skip to main content

Сallback handling

After the merchant's client has made a transaction to the wallet with the required amount, the transaction is processed by the processing side and a callback is sent to the merchant's backend.

swagger

Receiving Callback

Request

XAMAX processing sends a callback

POST https://exmpale.com/callback HTTP/1.1
Content-Length: 397
Cache-Control: no-cache
Authorization Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjMxN2NkNjA3MmE1ZDgxYWQwMjlmOTYzMzc0MTMxNWM5ZDczNTZiNWM1NzM0YTE5YmE0MDQ5MmQ1ODY1MGY5ZGQiLCJzaWdfYWxnIjoicGtjczF2MTUiLCJ0eXAiOiJKV1QifQ.eyJib2R5X2hhc2giOiJlY2ZiNDBmMTFkNDBlOTRmYzViMzNlZDM2YTljYTM0MjcwOTkzM2Y3ZTFlMDhlNTUxNDNlZTc3MmJiNzg0NmFiIiwiYm9keV9oYXNoX21ldGhvZCI6InNoYTI1NiIsImlzcyI6InhhbWF4LmlvIiwic3ViIjoicHJvY2Vzc2luZyIsImF1ZCI6WyJib2JAZXhhbXBsZS5jb20iXSwiZXhwIjoxNzA5MDMzMjEyLCJuYmYiOjE3MDkwMzI5MTMsImlhdCI6MTcwOTAzMjkxMiwianRpIjoiOTMzMTI1ODgtNmZkMC00OTI4LWIxNjItOWExOWMxODQ5OGU0In0.j77ChxeVNVfPpB5xAM-6olQTA52I6klv_KEAIRgJaUrqOC3vaHEqHEwB06bcgdEtUJKTSoWD0Ce74nYaFdF8yt2kk5zaafnF7s2PExJWfxwEv4Frz3X2xJXYSB1XypSeEJNeaVyvcwzWQYmAUuClNV50UvTEJH8VBgjGC668Vrw6ZV6Zx6GA5gb2lOwdIC9damm_0L0V1g6ww2DHPq68ag4r6stYWwoELRFl9dHil2XyqjNpmHd2RTnObrNEXn_D-rv-eQCObay_HwjMWsXjBYOsICsTZcqsQJbjFdu91GL158qWM5-FOuy3aAKm3gWertfHNt37mbmrngYaYZ6h8w
X-Resource-Type: incoming-transaction
User-Agent: xamax.io callback/1
Content-Type: application/json
Accept-Encoding: gzip

{"txId":2027,"walletAddress":"TBMczkFmXEzfpmQEghqFiVtss2fqsqSfhL","status":"transaction_status_confirmed","expiredAt":"2024-02-15T09:59:08Z","amountRequired":"26001000","amount":"26001000","code":"usdt_trc20","txHash":"f501644a6597a3b04194ace5d7af7a1de4bfb30624de9b6b4a87938f5b1e0401","confirmations":45,"exchangeRate":{"currency":"usd","exchange_rate":1,"currency_amount":26,"code":"usdt_trc20"}}
ParameterDescription
txIdThe identifier that was specified when creating the invoice
walletAddressThe address of the wallet to which the transaction came
codeCurrency code
amountRequiredThe amount with which the client must make a transaction to the wallet
expiredAtThe date the transaction will expire
statusStatus
txHashTransaction hash on the blockchain
amountReceived transaction amount
confirmationsThe number of confirmations in the blockchain

Transaction statuses

The main statuses for which we produce a callback

StatusValueDescription
Confirmedtransaction_status_confirmedTransaction confirmed in blockchain
Failedtransaction_status_failedTransaction failed in blockchain. In case of problems on the part of the blockchain, the transaction from waiting can go to failed.
Cancelledtransaction_status_canceledTransaction cancelled in processing. The cancelled status is not used, since it is necessary to catch the transaction before it goes to the blockchain, and this is a fairly short period of time. so not used yet. *reserved
Dusttransaction_status_dustTransaction incoming amount is small.
Refundedtransaction_status_refundedTransaction refunded

Сallback handling. Step-by-step

After receiving callback request you should handle all parameters

1. Get a header from callback request

Example of Authorization header

Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImZkY2IxYTdiZmY5OGNhMDkyOWY5MDBjNmU2ZmYxYzBkNGI4NmIzZDllMWRjMjM5OGU2NjlmYTRkYTA0ZTA3YTAiLCJzaWdfYWxnIjoicGtjczF2MTUiLCJ0eXAiOiJKV1QifQ.eyJib2R5X2hhc2giOiJmMTE1MmU2YjFhOWY1YmQ0ODhiZDk5MWFkMTgxNTYwNDM0ZmFmZDA5ZmRiNGMyOTFlMDM1MWQ0ODlkNzUxYzU2IiwiYm9keV9oYXNoX21ldGhvZCI6InNoYTI1NiIsImlzcyI6InhhbWF4LmlvIiwic3ViIjoicHJvY2Vzc2luZyIsImF1ZCI6WyJib2JAZXhhbXBsZS5jb20iXSwiZXhwIjoxNjcxNzQ0NTAxLCJuYmYiOjE2NzE3NDQyMDIsImlhdCI6MTY3MTc0NDIwMSwianRpIjoiYTJiZWRkMTUtNmJhNS00MmIzLThjZjItODk2ZWNiMDA2MWNhIn0.EOic90GWQEWPQEWkNi8pqf10ZAT3EMcWf7V-b7XqgG93TRH_HedQF3wisuZ5vY-OySuGcaROaTyWxiDWLFJ-ILNpzzqxCq8xuH8p8SlgQeLpYv7jh3DQyjQuMnWKEARJRN8QoeYqLE1jO1As7-3QqJIDuvb6sPo1C89VIVW1FqYwtPk8x2VLd0TeUk3Z18fD1YLqvc5Q2a8DWW_SsNOBCftAKk9YtBr1YCDpF_AxkE337Sb6YIW8_XCAEbYq8eCSw7DSrMfEMrWWnJJCGuvCFLEHv6KTrTg2mQ0Fppvbj3o1I6f_uDzQ90FfuZxaEN-iFuP5TH6SpO6PXPmrPZXWHA

2. Download JWKS public keys from our endpoint

A JWT session is transmitted, which can be confirmed using public JWKS keys.

You can get public keys by /.well-known/jwks.json request. You should use the key that has the same kid as in Authorization header

caution

Keys are changed every 7 days

GET https://api.xamax.io/.well-known/jwks.json HTTP/1.1

3. Parse JWT token by library

Libraries for Token Signing/Verification

// Parsed JWT header
{
"alg": "RS256",
"kid": "fdcb1a7bff98ca0929f900c6e6ff1c0d4b86b3d9e1dc2398e669fa4da04e07a0",
"sig_alg": "pkcs1v15",
"typ": "JWT"
}
// Parsed JWT payload
{
"body_hash": "f1152e6b1a9f5bd488bd991ad181560434fafd09fdb4c291e0351d489d751c56",
"body_hash_method": "sha256",
"iss": "xamax.io",
"sub": "processing",
"aud": [
"bob@example.com"
],
"exp": 1671744501,
"nbf": 1671744202,
"iat": 1671744201,
"jti": "a2bedd15-6ba5-42b3-8cf2-896ecb0061ca"
}

4. Check JWT expiration date and "aud" field

Checking the expiration time of the JWT token

// Parsed JWT payload
{
"body_hash": "f1152e6b1a9f5bd488bd991ad181560434fafd09fdb4c291e0351d489d751c56",
"body_hash_method": "sha256",
"iss": "xamax.io",
"sub": "processing",
"aud": [
"bob@example.com" // your email
],
"exp": 1671744501,
"nbf": 1671744202,
"iat": 1671744201,
"jti": "a2bedd15-6ba5-42b3-8cf2-896ecb0061ca"
}

5. Check kid and validate signature

Check kid in parsed JWT header and in JWKS key list

1 2 3 4 5 6 7 // JWT Header { "alg": "RS256", "kid": "fdcb1a7bff98ca0929f900c6e6ff1c0d4b86b3d9e1dc2398e669fa4da04e07a0", "sig_alg": "pkcs1v15", "typ": "JWT" }
1 2 3 4 5 6 7 // JWKS keys { "keys": [ { "kid": "fdcb1a7bff98ca0929f900c6e6ff1c0d4b86b3d9e1dc2398e669fa4da04e07a0" "e": "AQAB", "kty": "RSA"

Checking the validity of the signature based on public keys. Keys are rotated regularly, so you should not cache them for a long time. Ready-made libraries support the functionality when they update keys in the cache if the corresponding [kid] is not found (https://www.rfc-editor.org/rfc/rfc7517#section-4.5)

Validate JWT signature by JWKS keys. PHP example

$jwsVerifier = new JWSVerifier(new AlgorithmManager([new RS256()]));
$isVerified = $jwsVerifier->verifyWithKeySet($jws, $jwk, 0);
if (!$isVerified) {
throw new Exception("invalid signature");
}

7. Comparing the hash of the request body with the hash in the body of the JWT session

Request example

POST https://exmpale.com/callback HTTP/1.1
Content-Length: 397
Cache-Control: no-cache
Authorization Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjMxN2NkNjA3MmE1ZDgxYWQwMjlmOTYzMzc0MTMxNWM5ZDczNTZiNWM1NzM0YTE5YmE0MDQ5MmQ1ODY1MGY5ZGQiLCJzaWdfYWxnIjoicGtjczF2MTUiLCJ0eXAiOiJKV1QifQ.eyJib2R5X2hhc2giOiJlY2ZiNDBmMTFkNDBlOTRmYzViMzNlZDM2YTljYTM0MjcwOTkzM2Y3ZTFlMDhlNTUxNDNlZTc3MmJiNzg0NmFiIiwiYm9keV9oYXNoX21ldGhvZCI6InNoYTI1NiIsImlzcyI6InhhbWF4LmlvIiwic3ViIjoicHJvY2Vzc2luZyIsImF1ZCI6WyJib2JAZXhhbXBsZS5jb20iXSwiZXhwIjoxNzA5MDMzMjEyLCJuYmYiOjE3MDkwMzI5MTMsImlhdCI6MTcwOTAzMjkxMiwianRpIjoiOTMzMTI1ODgtNmZkMC00OTI4LWIxNjItOWExOWMxODQ5OGU0In0.j77ChxeVNVfPpB5xAM-6olQTA52I6klv_KEAIRgJaUrqOC3vaHEqHEwB06bcgdEtUJKTSoWD0Ce74nYaFdF8yt2kk5zaafnF7s2PExJWfxwEv4Frz3X2xJXYSB1XypSeEJNeaVyvcwzWQYmAUuClNV50UvTEJH8VBgjGC668Vrw6ZV6Zx6GA5gb2lOwdIC9damm_0L0V1g6ww2DHPq68ag4r6stYWwoELRFl9dHil2XyqjNpmHd2RTnObrNEXn_D-rv-eQCObay_HwjMWsXjBYOsICsTZcqsQJbjFdu91GL158qWM5-FOuy3aAKm3gWertfHNt37mbmrngYaYZ6h8w
X-Resource-Type: incoming-transaction
User-Agent: xamax.io callback/1
Content-Type: application/json
Accept-Encoding: gzip

{"txId":2027,"walletAddress":"TBMczkFmXEzfpmQEghqFiVtss2fqsqSfhL","status":"transaction_status_confirmed","expiredAt":"2024-02-15T09:59:08Z","amountRequired":"26001000","amount":"26001000","code":"usdt_trc20","txHash":"f501644a6597a3b04194ace5d7af7a1de4bfb30624de9b6b4a87938f5b1e0401","confirmations":45,"exchangeRate":{"currency":"usd","exchange_rate":1,"currency_amount":26,"code":"usdt_trc20"}}

Comparing the hash of the request body with the hash in the body of the JWT session. Key body_hash, hash type specified in body_hash_method. Before getting the hash of the request body, it is not necessary to parse it, sort the keys, and any other operations.

Step-by-Step:

Parse JWT payload and get body_hash and body_hash_method

// Parsed JWT payload from request example
{
"body_hash": "ecfb40f11d40e94fc5b33ed36a9ca342709933f7e1e08e55143ee772bb7846ab",
"body_hash_method": "sha256",
"iss": "xamax.io",
"sub": "processing",
"aud": [
"bob@example.com"
],
"exp": 1709033212,
"nbf": 1709032913,
"iat": 1709032912,
"jti": "93312588-6fd0-4928-b162-9a19c18498e4"
}

Take hash sha256 from body request

caution

Parse body request like string without json parsing

sha256('{"txId":2027,"walletAddress":"TBMczkFmXEzfpmQEghqFiVtss2fqsqSfhL","status":"transaction_status_confirmed","expiredAt":"2024-02-15T09:59:08Z","amountRequired":"26001000","amount":"26001000","code":"usdt_trc20","txHash":"f501644a6597a3b04194ace5d7af7a1de4bfb30624de9b6b4a87938f5b1e0401","confirmations":45,"exchangeRate":{"currency":"usd","exchange_rate":1,"currency_amount":26,"code":"usdt_trc20"}}')

result: ecfb40f11d40e94fc5b33ed36a9ca342709933f7e1e08e55143ee772bb7846ab

And compare body_hash from JWT payload with sha256 result. If all checks are passed, then the callback can be considered valid and the data that was passed in the request body can be trusted.

Code example

A simple example of request header and body validation

package main

import (
"crypto/sha256"
"fmt"
"log"

"github.com/MicahParks/keyfunc"
"github.com/golang-jwt/jwt/v4"
)

type Claims struct {
BodyHash string `json:"body_hash"`
BodyHashMethod string `json:"body_hash_method"`
jwt.RegisteredClaims
}

func main() {
var (
err error
jwks *keyfunc.JWKS
jwksUrlPath = "https://api.sandbox.xamax.io/.well-known/jwks.json"
)
// load jwks by URL
if jwks, err = keyfunc.Get(jwksUrlPath, keyfunc.Options{RefreshUnknownKID: true}); err != nil {
log.Fatalf("failed load JWKS: %s", err)
}

var (
// request header
header = `eyJhbGciOiJSUzI1NiIsImtpZCI6ImZkY2IxYTdiZmY5OGNhMDkyOWY5MDBjNmU2ZmYxYzBkNGI4NmIzZDllMWRjMjM5OGU2NjlmYTRkYTA0ZTA3YTAiLCJzaWdfYWxnIjoicGtjczF2MTUiLCJ0eXAiOiJKV1QifQ.eyJib2R5X2hhc2giOiJmMTE1MmU2YjFhOWY1YmQ0ODhiZDk5MWFkMTgxNTYwNDM0ZmFmZDA5ZmRiNGMyOTFlMDM1MWQ0ODlkNzUxYzU2IiwiYm9keV9oYXNoX21ldGhvZCI6InNoYTI1NiIsImlzcyI6InhhbWF4LmlvIiwic3ViIjoicHJvY2Vzc2luZyIsImF1ZCI6WyJib2JAZXhhbXBsZS5jb20iXSwiZXhwIjoxNjcxNzQ0NTAxLCJuYmYiOjE2NzE3NDQyMDIsImlhdCI6MTY3MTc0NDIwMSwianRpIjoiYTJiZWRkMTUtNmJhNS00MmIzLThjZjItODk2ZWNiMDA2MWNhIn0.EOic90GWQEWPQEWkNi8pqf10ZAT3EMcWf7V-b7XqgG93TRH_HedQF3wisuZ5vY-OySuGcaROaTyWxiDWLFJ-ILNpzzqxCq8xuH8p8SlgQeLpYv7jh3DQyjQuMnWKEARJRN8QoeYqLE1jO1As7-3QqJIDuvb6sPo1C89VIVW1FqYwtPk8x2VLd0TeUk3Z18fD1YLqvc5Q2a8DWW_SsNOBCftAKk9YtBr1YCDpF_AxkE337Sb6YIW8_XCAEbYq8eCSw7DSrMfEMrWWnJJCGuvCFLEHv6KTrTg2mQ0Fppvbj3o1I6f_uDzQ90FfuZxaEN-iFuP5TH6SpO6PXPmrPZXWHA`
// request body
data = `{"txId":400,"walletAddress":"0x2C5FE81093b9eA33B4017BfB3c646C1e1daCC2dD","status":"transaction_status_expired","expiredAt":"2022-12-21T18:47:27Z","amountRequired":"20000000","amount":"0","code":"usdt"}`

token *jwt.Token
claims Claims
)
// parse the JWT with claims
if token, err = jwt.ParseWithClaims(header, &claims, jwks.Keyfunc); err != nil {
log.Fatalf("Failed to parse JWT: %s", err)
}

// check JWT validation
if !token.Valid {
log.Fatal("invalid token")
}

// check merchant account email
if claims.Audience[0] != "bob@example.com" {
log.Fatal("incorrect merchant email")
}

// generate checksum from raw request body
var checksum = fmt.Sprintf("%x", sha256.Sum256([]byte(data)))

// compare checksum from jwt claims with generated body checksum
if claims.BodyHash != checksum {
log.Fatal("incorrect body hash")
}

// callback is valid
log.Println("ok")
}