Signed requests

Required on POST /v1/chain-payouts, and on POST /v1/deposit-keys if you ever hand us a wrapped key. A bearer key alone asks for nothing on these routes: the request must also be signed by a key we registered and verified for your tenant.

X-Bankcore-Request-Signature: v1,kid=<uuid>,ts=<unix seconds>,sig=<base64url>

Exactly these four fields, in this order, with no spaces. kid is lowercase. sig is base64url without padding.

What is signed

Scheme bankcore-request-v1: ECDSA P-256 / SHA-256, DER-encoded, over eight UTF-8 lines joined by LF, with no trailing LF:

bankcore-request-v1
<method>                 POST
<path>                   the URL path only, no query
<audience>               the value we give you at onboarding
<kid>                    your key id, lowercase
<ts>                     Unix seconds, decimal
<Idempotency-Key>        that header's value, exactly as sent
<sha256(body)>           lowercase hex of the raw body bytes

Example

import base64, hashlib, json, time
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec

private_key = serialization.load_pem_private_key(open("signing.pem", "rb").read(), None)

body = json.dumps(
    {
        "amount": {"amount": "500", "currency": "USDT_TRC20"},
        "network": "tron",
        "destination": {"address": "T...", "memo": None},
        "reference": "payout-000123",
    },
    separators=(",", ":"),
).encode()

kid = "<your key id>"
ts = int(time.time())
idempotency_key = "payout-000123"
lines = [
    "bankcore-request-v1",
    "POST",
    "/v1/chain-payouts",
    "<audience>",
    kid,
    str(ts),
    idempotency_key,
    hashlib.sha256(body).hexdigest(),
]
der = private_key.sign("\n".join(lines).encode(), ec.ECDSA(hashes.SHA256()))
sig = base64.urlsafe_b64encode(der).rstrip(b"=").decode()

headers = {
    "Authorization": "Bearer <your key>",
    "Idempotency-Key": idempotency_key,
    "X-Bankcore-Request-Signature": f"v1,kid={kid},ts={ts},sig={sig}",
    "Content-Type": "application/json",
}
# send exactly `body` with these headers

Registering your key

  1. Generate a P-256 key pair where your payout code runs. The private key never leaves you.
  2. Send us the public key as a DER SubjectPublicKeyInfo, base64.
  3. We register and verify it and give you its kid. Until then every signed call answers 401 signature_invalid.

Before your first call, check your signer against the test vector we send you: a test key pair, a full signing input and the signature it must produce. That key pair is a test one; never register it.

Refusals

CodeHTTP
signature_invalid401Missing or malformed header, a kid that is not a live key of yours, or a signature that does not verify. Nothing was done
signature_expired401ts outside ±300 s. Sign again
tenant_mismatch403The body names a tenant other than this key's. Nothing was recorded