Skip to content

Signing requests

Every request to the API is signed with your key. There are no sessions and no tokens: the signature is checked on each request.

Header Value
X-Merchant-Key Your key, mka_test_… or mka_….
X-Timestamp Unix time in seconds. Within ±5 minutes of the server’s clock.
X-Nonce 16–128 characters of A–Z a–z 0–9 _ -, new for every request, retries included.
X-Body-SHA256 Lowercase hex SHA-256 of the exact body bytes. Without a body, of the empty string.
X-Signature Lowercase hex HMAC-SHA256 of the canonical request, keyed with your secret.

Join five lines with \n (no trailing newline):

POST
/v1/payments
1792694400
0123456789abcdef0123456789abcdef
1981bce37ff314cc0a7187703d73a6d469440bebcef6aa05bd4319e20e0521d6
  1. The method, upper case.
  2. The path with its query, exactly as sent, without the host: /v1/payouts?limit=10.
  3. The timestamp of X-Timestamp.
  4. The nonce of X-Nonce.
  5. The body hash of X-Body-SHA256.

The HMAC key is the secret decoded from base64url, not the secret’s text. The secret is 43 characters of base64url without padding, 32 bytes once decoded.

Each language’s examples sign through one small module. Copy it into your project as it is.

examples/curl/signed-request.sh
# Signs and sends one request to the Merchant Payments API with curl and openssl.
# Source it from bash: . signed-request.sh
# MKA_KEY, MKA_SECRET and MKA_BASE_URL come from the environment, never from code.
: "${MKA_BASE_URL:=https://sandbox.processing.yolotech.cc}"
# The HMAC key as hex: the secret is base64url text, the key the bytes it encodes.
mka_hmac_key() {
local secret padded
secret=$(printf '%s' "$MKA_SECRET" | tr '_-' '/+')
padded="$secret$(printf '%*s' $(( (4 - ${#secret} % 4) % 4 )) '' | tr ' ' '=')"
printf '%s' "$padded" | openssl base64 -d -A | od -An -v -tx1 | tr -d ' \n'
}
# mka_request METHOD TARGET [BODY] [IDEMPOTENCY_KEY]
# TARGET is the path with its query. Prints the status line, then the body.
mka_request() {
local method=$1 target=$2 body=${3:-} key=${4:-}
local timestamp nonce body_hash signature
timestamp=$(date +%s)
nonce=$(openssl rand -hex 16) # new on every request, retries included
body_hash=$(printf '%s' "$body" | openssl dgst -sha256 -r | cut -d' ' -f1)
signature=$(printf '%s\n%s\n%s\n%s\n%s' "$method" "$target" "$timestamp" "$nonce" "$body_hash" |
openssl dgst -sha256 -mac HMAC -macopt "hexkey:$(mka_hmac_key)" -r | cut -d' ' -f1)
local headers=(-H "X-Merchant-Key: $MKA_KEY" -H "X-Timestamp: $timestamp" -H "X-Nonce: $nonce"
-H "X-Body-SHA256: $body_hash" -H "X-Signature: $signature")
[ -n "$body" ] && headers+=(-H "Content-Type: application/json" --data-binary "$body")
[ -n "$key" ] && headers+=(-H "Idempotency-Key: $key")
# MKA_DRY_RUN=1 prints the signed request instead of sending it.
if [ -n "${MKA_DRY_RUN:-}" ]; then
printf '{"method":"%s","target":"%s","body":%s,"headers":{"X-Merchant-Key":"%s","X-Timestamp":"%s","X-Nonce":"%s","X-Body-SHA256":"%s","X-Signature":"%s"}}\n' \
"$method" "$target" "$(printf '%s' "$body" | mka_json_string)" "$MKA_KEY" "$timestamp" "$nonce" "$body_hash" "$signature"
return
fi
local head answer status replayed=''
head=$(mktemp)
answer=$(curl -sS -X "$method" "${headers[@]}" -D "$head" -w '\n%{http_code}' "$MKA_BASE_URL$target") || { rm -f "$head"; return 1; }
grep -qi '^idempotent-replayed: *true' "$head" && replayed=' (replayed)'
rm -f "$head"
status=${answer##*$'\n'}
printf 'HTTP %s%s\n' "$status" "$replayed"
if [ -n "${answer%$'\n'*}" ]; then printf '%s\n' "${answer%$'\n'*}"; fi
}
# A JSON string of stdin, for the dry run.
mka_json_string() {
sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | awk 'BEGIN{ORS=""; printf "\""} NR>1{printf "\\n"} {print} END{printf "\""}'
}
# A new Idempotency-Key. Keep it with the request you retry: a retry reuses it.
mka_new_key() { openssl rand -hex 16; }
# An input the example needs, from the environment.
mka_required() {
if [ -z "${!1:-}" ]; then echo "set $1" >&2; exit 1; fi
printf '%s' "${!1}"
}

Your code should produce these signatures for these inputs. The server’s own tests check the same values.

Input Value
Secret MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE
Timestamp 1792694400
Nonce 0123456789abcdef0123456789abcdef
Request Body Signature
GET /v1/payments/019fccd2-3eef-4a33-80b8-cd750e2db99f empty 56d354416d979093c536ba60b1f267ccec4fd0db1ad0490b26b911d238be6be5
POST /v1/payments {"merchant_order_id":"order-1001","amount":"2500.00","currency":"LKR"} ccee5a223a9d2047bd93fc1ad9144cec5a27de6c1b76724c24a2ae068776cc8a

Every example also has a dry run: MKA_DRY_RUN=1 prints the signed request instead of sending it.

Every authentication failure is the same 401 with {"title":"Unauthorized","status":401,"detail":"authentication failed"}, on purpose. Check, in this order:

  • The key and the installation. mka_test_ keys work only in the sandbox; a revoked key, or one replaced more than 24 hours ago, stops at once.
  • The HMAC key. Decode the secret from base64url; do not use its text.
  • The body. Hash and send the same bytes. Serialising JSON twice — once to hash, once to send — gives different bytes in some languages.
  • The path. Sign the path with its query, escaped as sent, without the host.
  • The clock. Keep the server on NTP; more than 5 minutes off is refused.
  • The nonce. A nonce is used once. A retry needs a new nonce and timestamp — and the same Idempotency-Key.