Signing requests
Every request carries two headers:
| Header | Value |
|---|---|
x-api-key | Your API key for this environment |
x-signature | Lowercase hex HMAC-SHA256 of the canonical body, keyed with your secret key |
The body also contains:
| Field | Rule |
|---|---|
timestamp | Unix time in seconds, UTC. Must be within 5 minutes of InsPay’s clock. |
nonce | A unique random string. Do not reuse it. |
Sign the canonical string, then send those exact bytes. A pretty-printed body, a different key order, or raw katakana instead of \u escapes produces Invalid signature.
Use placeholders in your own notes. This guide uses YOUR_API_KEY and YOUR_SECRET_KEY.
Canonical string
Drop empty fields
Take every field in the body. Drop fields whose value is null. Do not include the key.
Stringify every value
Convert each remaining value with Python’s str: 1000 becomes "1000". The published fields are strings and numbers, so JavaScript String() matches for those. The API does not use booleans. Python’s str(True) is True, which is not JavaScript’s true.
Serialise compact JSON
Sort the keys lexicographically. Serialise as JSON with no whitespace, the same as Python json.dumps(..., sort_keys=True, separators=(",", ":")).
That call escapes non-ASCII as lowercase \uXXXX. Characters above U+FFFF use a UTF-16 surrogate pair. A space stays a space. / is not escaped.
HMAC-SHA256
HMAC-SHA256(key = secret key, message = canonical string), hex-encoded, lowercase. The secret is the UTF-8 secret key. The message is the UTF-8 canonical string.
Send that canonical string as the HTTP body, with Content-Type: application/json.
Worked example
Secret key: YOUR_SECRET_KEY
Logical body, before canonicalisation. timestamp here is 11 October 2026, 14:32:27 UTC, so you can reproduce the digest. A live call needs a current timestamp or InsPay returns Invalid or expired timestamp.
{
"amount": 50000,
"channel": "BANK_TRANSFER",
"currency": "JPY",
"callbackUrl": "https://merchant.example.com/callbacks/deposit",
"referenceId": "TXN-20261011-001",
"playerId": "player_abc123",
"payerAccountName": "ヤマダ タロウ",
"timestamp": 1791729147,
"nonce": "b9ea6ab75157aa21"
}Canonical string. This is what you hash and what you POST. The payer name is escaped:
{"amount":"50000","callbackUrl":"https://merchant.example.com/callbacks/deposit","channel":"BANK_TRANSFER","currency":"JPY","nonce":"b9ea6ab75157aa21","payerAccountName":"\u30e4\u30de\u30c0 \u30bf\u30ed\u30a6","playerId":"player_abc123","referenceId":"TXN-20261011-001","timestamp":"1791729147"}Signature:
0c6e76542907f62ed857166a5ac0f88eb983b5f1492b66e9a8fa4ffccbe43bc5If your code prints a different hex for this body and YOUR_SECRET_KEY, you are hashing different bytes. Compare your canonical string to the one above before you change the HMAC call.
Reference clients
These three clients reproduce the worked example. Swap in a fresh timestamp and nonce for a live sandbox call, and replace the placeholder keys.
API_KEY="YOUR_API_KEY"
SECRET_KEY="YOUR_SECRET_KEY"
BASE_URL="https://api-sandbox.inspay.dev/merchant"
# Exact canonical bytes from the worked example. No trailing newline.
CANONICAL='{"amount":"50000","callbackUrl":"https://merchant.example.com/callbacks/deposit","channel":"BANK_TRANSFER","currency":"JPY","nonce":"b9ea6ab75157aa21","payerAccountName":"\u30e4\u30de\u30c0 \u30bf\u30ed\u30a6","playerId":"player_abc123","referenceId":"TXN-20261011-001","timestamp":"1791729147"}'
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$SECRET_KEY" -hex | awk '{print $NF}')
# Expected with YOUR_SECRET_KEY:
# 0c6e76542907f62ed857166a5ac0f88eb983b5f1492b66e9a8fa4ffccbe43bc5
curl -sS -X POST "$BASE_URL/deposit" \
-H "Content-Type: application/json" \
-H "x-api-key: $API_KEY" \
-H "x-signature: $SIG" \
--data-binary "$CANONICAL"Production uses the same algorithm and a different base URL: https://api.inspay.dev/merchant.
What usually breaks the signature
- Hashing the pretty JSON, or a body whose keys are not sorted.
- Leaving a number as a JSON number in the hashed string. The hashed values are strings:
"50000", not50000. - Sending katakana as raw UTF-8 inside the hashed JSON. The canonical form uses
\u30e4style escapes. - Adding a trailing newline before the HMAC.
printf '%s'avoids that. Pythonjson.dumpsdoes not add one. - Signing with the sandbox secret and calling production, or the reverse.
- Reusing a
nonce, or atimestampolder than five minutes. Those fail even when the HMAC math is right. - Dropping a field after you sign, or letting a client re-serialise the body.
A bad signature is 400 today. Coming soonThe status for a bad signature becomes 401. A missing or unknown API key is already 401.
Callback signatures
Coming soonCallbacks will carry x-signature. It is the same HMAC over the callback body, using your secret key. Verify it before you credit a player. The check is shown on the callbacks page.