Quickstart¶
Go from nothing to a verified signed request against the Yanez test environment. Budget about ten minutes.
You need a backend you can run code on. Everything here happens server-side — the partner private key must never reach a browser or a mobile app.
What you get at the end
A working request signer, and a 200 from the record validation endpoint
proving Yanez accepts your signature.
1. Generate a signing key pair¶
Yanez authenticates partners with Ed25519 request signatures. You generate the key pair; Yanez only ever sees the public half.
import base64
import json
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import (
Encoding, NoEncryption, PrivateFormat, PublicFormat,
)
def b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
priv = Ed25519PrivateKey.generate()
raw_priv = priv.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
raw_pub = priv.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
print("Private key hex (store as a backend secret):", raw_priv.hex())
print("Public JWK (send to Yanez):", json.dumps(
{"kty": "OKP", "crv": "Ed25519", "x": b64url(raw_pub)}
))
Run it once with pip install cryptography. Put the private key hex in your
backend secret store. Never commit it.
2. Onboard with Yanez¶
Send Yanez the public JWK and a key id you choose, such as
partner_key_2026_01. See Onboarding for the full list of
details to supply.
Yanez sends back:
| Value | Example | Where it goes |
|---|---|---|
partner_id |
ptr_abc123 |
Backend config |
kid |
partner_key_2026_01 |
Backend config |
| Base URL | https://ptest.yanez.ai |
Backend config |
You cannot complete step 4 until Yanez confirms your key is active.
3. Build the request signer¶
This signer authenticates the requests your backend sends to the Yanez server APIs, such as the validation call in step 4. It's separate from deep link signing, which signs the links you hand to users. Every signed request carries seven X-Yanez-* headers. The signature covers a canonical string — not the raw HTTP request — built by joining these nine lines
with \n, with no trailing newline:
YANEZ-SIG-V1
<HTTP_METHOD_UPPERCASE>
<PATH_WITH_QUERY>
<X-Yanez-Partner-Id>
<X-Yanez-Key-Id>
<X-Yanez-Signature-Alg>
<X-Yanez-Timestamp>
<X-Yanez-Nonce>
<X-Yanez-Content-SHA256>
import base64
import hashlib
import secrets
from datetime import datetime, timezone
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
PARTNER_ID = "ptr_abc123"
KEY_ID = "partner_key_2026_01"
PRIVATE_KEY_HEX = "..." # 32-byte Ed25519 private key, hex
def b64url(raw: bytes) -> str:
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
def sign(method: str, path_with_query: str, body: bytes = b"") -> dict:
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
nonce = secrets.token_urlsafe(18)
body_hash = b64url(hashlib.sha256(body).digest())
canonical = "\n".join([
"YANEZ-SIG-V1", method.upper(), path_with_query,
PARTNER_ID, KEY_ID, "EdDSA", timestamp, nonce, body_hash,
]).encode()
key = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(PRIVATE_KEY_HEX))
return {
"Content-Type": "application/json",
"X-Yanez-Partner-Id": PARTNER_ID,
"X-Yanez-Key-Id": KEY_ID,
"X-Yanez-Signature-Alg": "EdDSA",
"X-Yanez-Timestamp": timestamp,
"X-Yanez-Nonce": nonce,
"X-Yanez-Content-SHA256": body_hash,
"X-Yanez-Signature": b64url(key.sign(canonical)),
}
import crypto from "node:crypto";
const PARTNER_ID = "ptr_abc123";
const KEY_ID = "partner_key_2026_01";
const PRIVATE_KEY_HEX = "..."; // 32-byte Ed25519 private key, hex
// Node needs a PKCS#8 DER key; for Ed25519 the 16-byte header is constant.
const PKCS8_ED25519_HEADER = Buffer.from("302e020100300506032b657004220420", "hex");
const b64url = (buf) => buf.toString("base64url");
function privateKey() {
return crypto.createPrivateKey({
key: Buffer.concat([PKCS8_ED25519_HEADER, Buffer.from(PRIVATE_KEY_HEX, "hex")]),
format: "der",
type: "pkcs8",
});
}
export function sign(method, pathWithQuery, body = Buffer.alloc(0)) {
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
const nonce = b64url(crypto.randomBytes(18));
const bodyHash = b64url(crypto.createHash("sha256").update(body).digest());
const canonical = [
"YANEZ-SIG-V1", method.toUpperCase(), pathWithQuery,
PARTNER_ID, KEY_ID, "EdDSA", timestamp, nonce, bodyHash,
].join("\n");
// Ed25519 signs the message directly — the digest argument must be null.
const signature = crypto.sign(null, Buffer.from(canonical), privateKey());
return {
"Content-Type": "application/json",
"X-Yanez-Partner-Id": PARTNER_ID,
"X-Yanez-Key-Id": KEY_ID,
"X-Yanez-Signature-Alg": "EdDSA",
"X-Yanez-Timestamp": timestamp,
"X-Yanez-Nonce": nonce,
"X-Yanez-Content-SHA256": bodyHash,
"X-Yanez-Signature": b64url(signature),
};
}
POST /api/partners/records/validate HTTP/1.1
Host: ptest.yanez.ai
Content-Type: application/json
X-Yanez-Partner-Id: ptr_abc123
X-Yanez-Key-Id: partner_key_2026_01
X-Yanez-Signature-Alg: EdDSA
X-Yanez-Timestamp: 2026-06-26T17:00:00Z
X-Yanez-Nonce: req_01JABCDEF456
X-Yanez-Content-SHA256: XDkZIoM5W0EY1xr-P3B_JetGPzeWRIW26sYNDYDMswo
X-Yanez-Signature: gf8tD2LfN9EdKzTUDBV_dtf4PJ-gSKSlslQkk4RDRsN...
{"yid":"796aaf67d496447fa4059dcea80b03aa"}
Both the content hash and the signature are base64url without padding. The hash above is the real SHA-256 of the body shown — useful to check your own encoder against.
There is no cURL one-liner for this — the signature has to be computed by your backend before the request is sent.
Serialize the body exactly once
Hash, sign, and send the same bytes. Re-serializing the JSON between
hashing and sending is the single most common cause of a 401.
4. Make your first call¶
Validate a record. An unknown yid is not an error — it returns 200 with
yid_valid: false, which still proves your signature was accepted.
import json
import requests
from yanez_sign import sign
BASE_URL = "https://ptest.yanez.ai"
PATH = "/api/partners/records/validate"
body = json.dumps({"yid": "0" * 32}).encode()
response = requests.post(BASE_URL + PATH, data=body, headers=sign("POST", PATH, body), timeout=10)
print(response.status_code, response.json())
| You see | Meaning |
|---|---|
200 {"yid_valid": false, "public_key_valid": null} |
Signature accepted. You are integrated. |
401 |
Signature rejected. See the troubleshooting table below. |
422 |
Body is missing yid, has an unknown field, or is invalid JSON. |
429 |
Over 60 requests per 60 seconds from your IP. Back off and retry. |
5. Troubleshoot a 401¶
Yanez returns a deliberately generic 401 that never says which check failed.
Work down this list:
| Check | Common mistake |
|---|---|
| Clock skew | Server time must be within ±300 seconds. Enable NTP. |
| Body hash | Hashed a re-serialized copy of the body instead of the bytes sent. |
| Path | PATH_WITH_QUERY must match what you send, byte for byte. Do not sort query parameters or re-encode after signing. |
| Nonce reuse | Nonces are single-use per partner and key for 600 seconds. Generate a fresh one per attempt, including retries. |
| Key not active | Yanez has not activated your kid yet, or it is outside its validity window. |
| Algorithm mismatch | X-Yanez-Signature-Alg must be EdDSA and match the registered key. |
| Encoding | Signatures and hashes are base64url without padding, not standard base64 or hex. |
Next steps¶
You now have an authenticated backend. The user-facing flow is next:
-
1. Launch YanezYID
Build and sign an HTTPS deep link (
{DEEP_LINK_BASE}?...), then deliver it as a QR code or tappable link. -
2. Receive the result
YanezYID posts a BLS-signed result to your callback. Verify it yourself — the payload carries no trusted status flag.
-
3. Confirm the binding
Validate the
(yid, public_key)pair against the Yanez registry before you persist it. -
4. Go live
Work the launch gates and the production checklist with your Yanez contact.