Deep Link Signing¶
YanezYID opens partner-initiated flows via a signed deep link. Partners generate the deep link on the partner backend and deliver it to the user as a QR code or tappable link.
Deliver the deep link as an HTTPS App Link (an iOS Universal Link / Android
App Link). The yanezbio:// custom scheme is
deprecated: it still works, but migrate to the
HTTPS form.
URL Format¶
{DEEP_LINK_BASE}?message=<b64url>&callback=<url>&method=post&request_id=<uuid>&event=<event>&description=<text>&app_id=<partner_id>[&kid=<kid>]&app_sig=<b64url>
| Environment | DEEP_LINK_BASE |
|---|---|
| Production | https://yid.yanez.ai/open |
| Partner test (ptest) | https://ptest.yanez.ai/open |
There is one exact path, /open, with nothing beneath it (/open/foo is a
404). Everything the link needs to carry rides in the query string.
app_sig is computed over the link exactly as delivered —
{DEEP_LINK_BASE}?<query> for the HTTPS form — see Signing.
| Parameter | Required | Notes |
|---|---|---|
message |
yes | Base64url-encoded payload the app signs verbatim (see Message Payload). |
callback |
yes | URL YanezYID delivers the result to — an HTTPS endpoint on your backend for method=post, or any URL the phone can open for method=redirect. Percent-encode it. There is no callback-domain allowlist. See Delivery Modes. |
method |
yes | post or redirect — selects callback delivery (case-insensitive). Must be present and non-empty or the link is rejected; an unrecognized value falls back to redirect. See Delivery Modes. |
request_id |
yes | Unique per-request identifier (a UUID works, but the app treats it as an opaque string). Use a fresh value per request. |
event |
yes | See Supported Events. |
description |
yes | Short human-readable label shown in YanezYID. |
app_id |
yes | Your partner_id (e.g. ptr_416582cd9ea9bb6c3750de1c). |
kid |
no | Signing key id. Include if you manage multiple keys. |
app_sig |
yes | Ed25519 signature. Must be the last parameter. |
How the Link Resolves¶
iOS (Universal Links) and Android (App Links) hand the link to YanezYID when
it's installed and has claimed the domain; otherwise the link opens normally
in the browser and {DEEP_LINK_BASE} 302-redirects the user to the App Store
or Google Play automatically, based on their device. This is what makes the
HTTPS form safe to render as a QR code: it never dead-ends.
Supported Events¶
| Event | event value |
|---|---|
| Enroll a user | enroll |
| Transaction signing | transaction.signed |
event is a required, non-empty string; YanezYID treats it as opaque and
does not validate or reject the value. Use the values above — transaction.signed
is the canonical type the Yanez backend records for a signing. (Some test
tooling emits transaction_sign; because the value is opaque to the app, either
is accepted, but prefer transaction.signed for consistency with the backend.)
Message Payload¶
The message parameter is the payload YanezYID signs. The only hard
requirement is that it is valid base64url — the decoded bytes are otherwise
unconstrained. The recommended shape for those bytes is a JSON object:
{
"v": 1,
"rp": "<your RP domain>",
"request_id": "<same uuid as request_id param>",
"user_id": "<your internal user id>",
"action": "<event>",
"ref": "<event>:<user_id>",
"required_tier": null,
"nonce": "<random string>",
"iat": 1782395776,
"exp": 1783000576
}
JSON is a recommendation, not a requirement. YanezYID does not parse or
validate these fields: it treats the decoded message as opaque bytes, signs
them verbatim (BLS), and echoes the same base64url string back in the callback.
Your backend then verifies the callback byte-for-byte against the challenge it
stored (see Verification Steps), and
correlation happens through request_id — so any decoded bytes work, even a
random nonce.
The JSON shape is recommended because it makes the signed bytes
self-describing: the BLS signature then attests to what was approved (rp,
user_id, action, exp), which helps with auditing and gives your callback
handler an independent expiry and user-binding check on top of the challenge
store. Whatever shape you choose, keep the exact bytes stable, since the
signature is computed over them.
Signing¶
The app verifies app_sig against your registered public key. The signature is
computed over the link exactly as you deliver it — every byte of the URL
before &app_sig=, base included. For the HTTPS App Link that is
{DEEP_LINK_BASE}?<query>; for the deprecated custom scheme it is
yanezbio://sign?<query>.
To sign:
- Build the canonical string
{DEEP_LINK_BASE}?<query>with every parameter exceptapp_sig, in the order shown above. - Take the exact UTF-8 bytes of that string — do not re-order, percent-decode, or normalize.
- Sign with your Ed25519 private key.
- Base64url-encode the 64-byte signature.
- Append
&app_sig=<signature>as the final parameter and deliver that exact string.
The custom scheme still works
The deprecated yanezbio:// scheme is still supported: build and sign the
canonical string yanezbio://sign?<query> the same way, and the link opens
YanezYID when the app is installed on the device. When the app is not
installed, the custom scheme does not take the user to the App Store or
Google Play automatically — nothing happens.
Sign the link you deliver
The signed bytes and the delivered link must match byte for byte, base
included. A signature computed over one base and delivered on another — for
example signing yanezbio://sign?... and delivering the query on
https://yid.yanez.ai/open?... — is rejected by the app with "Untrusted
signing request".
import base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
def b64url(raw: bytes) -> str:
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
def sign_deep_link(unsigned_link: str, private_key_hex: str) -> str:
"""`unsigned_link` is the link exactly as delivered, with every parameter except app_sig."""
private_key = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(private_key_hex))
sig = b64url(private_key.sign(unsigned_link.encode()))
return f"{unsigned_link}&app_sig={sig}"
Example (Enroll)¶
import base64, json, secrets, time, urllib.parse, uuid
PARTNER_ID = "ptr_..." # your partner_id
PRIVATE_KEY_HEX = "..." # 32-byte Ed25519 private key, hex
DEEP_LINK_BASE = "https://yid.yanez.ai/open" # https://ptest.yanez.ai/open in partner test
request_id = str(uuid.uuid4())
now = int(time.time())
message = json.dumps({
"v": 1,
"rp": "your.rp.domain",
"request_id": request_id,
"user_id": "your-internal-user-id",
"action": "enroll",
"ref": "enroll:your-internal-user-id",
"required_tier": None,
"nonce": secrets.token_urlsafe(32),
"iat": now,
"exp": now + 604800,
}, separators=(",", ":"))
encoded_message = base64.urlsafe_b64encode(message.encode()).rstrip(b"=").decode()
callback = "https://your-backend.example.com/api/yanez/callback"
query = (
f"message={encoded_message}"
f"&callback={urllib.parse.quote(callback, safe='')}"
f"&method=post"
f"&request_id={request_id}"
f"&event=enroll"
f"&description={urllib.parse.quote('Yanez Enrollment', safe='')}"
f"&app_id={PARTNER_ID}"
)
# Sign the link exactly as it will be delivered.
deep_link = sign_deep_link(f"{DEEP_LINK_BASE}?{query}", PRIVATE_KEY_HEX)
# -> https://yid.yanez.ai/open?message=...&app_id=ptr_...&app_sig=...
Custom Scheme (Deprecated)¶
Deprecated — migrate to the HTTPS App Link
The yanezbio:// custom scheme is deprecated. It still works, but it fails
silently when the app isn't installed — nothing happens, with no way to
recover — so it must not be used for new integrations. Migrate existing
integrations to the HTTPS URL format; the query string and
signing are unchanged.
The deprecated form carries the same parameters on a different base:
yanezbio://sign?message=<b64url>&callback=<url>&method=post&request_id=<uuid>&event=<event>&description=<text>&app_id=<partner_id>[&kid=<kid>]&app_sig=<b64url>
Production builds on both platforms register yanezbio://sign. Test builds
differ by platform:
| Build | iOS | Android |
|---|---|---|
| Production | yanezbio://sign |
yanezbio://sign |
| Dev | yanezbio-dev://sign only |
yanezbio://sign (also yanezbio-dev://sign) |
| Partner test | yanezbio-partner://sign only |
yanezbio://sign (also yanezbio-partner://sign) |
An iOS test build does not respond to yanezbio://sign — use its suffixed
scheme and sign the link exactly as delivered, suffixed scheme included. On
Android use yanezbio://sign on every flavor.
Signing is identical to the HTTPS case: app_sig covers the delivered link —
here yanezbio://sign?<query> — with every parameter except app_sig,
appended last — see Signing.
Common Errors¶
| Message | Cause |
|---|---|
| "Untrusted signing request" | app_sig is missing, malformed, not the last parameter, or doesn't verify against the registered public key — including a signature computed over a different base than the one delivered (for example, signed yanezbio://sign?... but delivered on the HTTPS base). Also shown when app_id or another required parameter is absent. |
| "This signing request could not be verified" | The app could not fetch the partner's keys: app_id is not registered in this app's environment (a ptest partner_id on the production app, or vice versa), the partner has no active key, or the device is offline. |