Skip to Content
Version 1.1 · A Coming soon badge is a backward-compatible change still rolling out.
Signing requests

Signing requests

Every request carries two headers:

HeaderValue
x-api-keyYour API key for this environment
x-signatureLowercase hex HMAC-SHA256 of the canonical body, keyed with your secret key

The body also contains:

FieldRule
timestampUnix time in seconds, UTC. Must be within 5 minutes of InsPay’s clock.
nonceA 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.

deposit-logical.json
{ "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:

canonical.txt
{"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:

signature.txt
0c6e76542907f62ed857166a5ac0f88eb983b5f1492b66e9a8fa4ffccbe43bc5

If 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.

sign.sh
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", not 50000.
  • Sending katakana as raw UTF-8 inside the hashed JSON. The canonical form uses \u30e4 style escapes.
  • Adding a trailing newline before the HMAC. printf '%s' avoids that. Python json.dumps does not add one.
  • Signing with the sandbox secret and calling production, or the reverse.
  • Reusing a nonce, or a timestamp older 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.

Last updated on