Device Authentication
Device key registration and transfer confirmation assertions.
Device-Bound Authentication
This guide explains how the partner app should register a customer device and use that device to confirm sensitive transfer actions.
Use this host in API examples:
https://api.yes.cash
---
1. Purpose
Device-bound authentication links a customer session to a cryptographic key held on the customer device.
The partner app uses this device key to:
- register the customer device with YesCash;
- prove that the private key is held by the device;
- sign transfer confirmation assertions when required;
- reduce the risk of unauthorized transfer confirmation from a copied session.
The private key must stay on the customer device.
---
2. What the partner app must build
The partner app must implement:
1. device key generation;
2. secure private key storage;
3. public key registration;
4. registration proof signing;
5. device assertion signing for transfer confirmation;
6. device re-registration handling;
7. device error recovery;
8. local device registration state tracking.
---
3. Required cryptographic standard
| Item | Requirement |
|---|---|
| Algorithm | RS256 |
| Key type | RSA |
| Minimum key size | RSA-2048 |
| Recommended key size | RSA-3072 |
| Payload canonicalisation | JCS |
| Signature format | Compact JWS |
| Private key location | Customer device secure storage |
| Public key location | Registered with YesCash API |
---
4. Device key storage rules
The partner app must generate and store the private key securely on the customer device.
Recommended storage:
| Platform | Recommended storage |
|---|---|
| iOS | Keychain / Secure Enclave where available |
| Android | Android Keystore / hardware-backed key where available |
| Web | WebAuthn/passkey-style secure authenticator where supported, or approved browser crypto model |
| Desktop | OS secure key store where supported |
Do not:
- send the private key to partner backend servers;
- send the private key to YesCash;
- store the private key in plain text;
- include the private key in logs;
- export the private key for backup;
- copy the private key between devices.
---
5. Device registration flow
The device registration flow has two API calls:
Customer authenticated
→ Start device registration
→ API returns challenge
→ App generates or selects device key
→ App signs registration proof
→ Complete device registration
→ API stores public key
Endpoints:
POST /v1/auth/device-registration/start
POST /v1/auth/device-registration/complete
---
6. Step 1 — Start device registration
The customer must be authenticated.
POST https://api.yes.cash/v1/auth/device-registration/start
Authorization: Bearer <access-token>
Ocp-Apim-Subscription-Key: <subscription-key>
X-Correlation-Id: <uuid>
Content-Type: application/json
The API returns a short-lived registration challenge.
Example response shape:
{
"registrationId": "DREG-01HX9F2J7K3M5N7P9Q1R3T5V7W",
"registrationChallenge": "challenge-value",
"expiresAt": "2026-05-11T15:30:12Z"
}
Partner app action:
- store
registrationIdtemporarily; - store
registrationChallengetemporarily; - generate the device key pair if not already generated;
- build and sign the registration proof.
---
7. Step 2 — Generate device key pair
Generate an RSA key pair on the device.
Requirements:
- RSA-2048 minimum;
- RSA-3072 recommended;
- private key marked non-exportable where platform supports it;
- key stored under OS secure storage;
- public key exportable for registration.
Recommended local metadata:
{
"deviceKeyId": "local-device-key-001",
"algorithm": "RS256",
"createdAt": "2026-05-11T14:30:12Z",
"storage": "platform-secure-store"
}
This metadata is local to the app. It is not a replacement for the public key registered with YesCash.
---
8. Step 3 — Build registration proof payload
The registration proof binds:
- registration challenge;
- registration ID;
- public key;
- device metadata;
- scheme version.
Example payload:
{
"registration_proof_v1": "v1",
"registration_id": "DREG-01HX9F2J7K3M5N7P9Q1R3T5V7W",
"registration_challenge": "challenge-value",
"device_key_alg": "RS256",
"device_public_key_jwk": {
"kty": "RSA",
"kid": "device-key-001",
"use": "sig",
"alg": "RS256",
"n": "<base64url-modulus>",
"e": "AQAB"
},
"iat": 1777102212
}
The payload must be canonicalised using JCS before signing.
---
9. Step 4 — Sign registration proof
The partner app signs the registration proof with the device private key.
JWS protected header:
{
"alg": "RS256",
"kid": "device-key-001",
"typ": "JWT"
}
Compact JWS format:
<base64url(header)>.<base64url(payload)>.<base64url(signature)>
---
10. Step 5 — Complete device registration
POST https://api.yes.cash/v1/auth/device-registration/complete
Authorization: Bearer <access-token>
Ocp-Apim-Subscription-Key: <subscription-key>
X-Correlation-Id: <uuid>
Content-Type: application/json
Example request shape:
{
"registrationId": "DREG-01HX9F2J7K3M5N7P9Q1R3T5V7W",
"devicePublicKey": {
"kty": "RSA",
"kid": "device-key-001",
"use": "sig",
"alg": "RS256",
"n": "<base64url-modulus>",
"e": "AQAB"
},
"registrationProof": "<compact-jws>"
}
Example response shape:
{
"deviceId": "DEV-01HX9F2J7K3M5N7P9Q1R3T5V7W",
"status": "ACTIVE",
"registeredAt": "2026-05-11T14:31:12Z"
}
Partner app action:
- store
deviceId; - mark the device as registered;
- continue to Core API flows.
---
11. Transfer confirmation flow
Some transfers require device-bound confirmation.
Flow:
Transfer requires confirmation
→ App reads transfer detail
→ API returns confirmation challenge
→ App builds assertion payload
→ Device signs assertion
→ App submits confirmation
→ API verifies assertion
Endpoint:
POST /v1/core/transfers/{transferId}/confirm
---
12. Step 1 — Read transfer detail
GET https://api.yes.cash/v1/core/transfers/{transferId}
Authorization: Bearer <access-token>
Ocp-Apim-Subscription-Key: <subscription-key>
X-Correlation-Id: <uuid>
The response may include:
{
"transferId": "TRF-01HX9F2J7K3M5N7P9Q1R3T5V7W",
"transferStatus": "VALIDATED",
"confirmationRequired": true,
"confirmationChallenge": "challenge-value",
"confirmationChallengeExpiresAt": "2026-05-11T15:35:12Z"
}
Partner app action:
- check
confirmationRequired; - check challenge expiry;
- show customer confirmation screen;
- build the device assertion from the latest transfer details.
---
13. Step 2 — Build transfer confirmation assertion
The transfer confirmation assertion should bind the customer confirmation to the exact transfer details shown to the customer.
Example payload:
{
"auth_signature_v1": "v1",
"transfer_id": "TRF-01HX9F2J7K3M5N7P9Q1R3T5V7W",
"challenge": "challenge-value",
"nonce": "b3f077a8-2930-4555-91ac-4ad6d5dbf51d",
"send_amount": "100.00",
"send_currency": "EUR",
"receive_amount": "1085.00",
"receive_currency": "MAD",
"beneficiary_id": "BEN-01HX9F2J7K3M5N7P9Q1R3T5V7W",
"iat": 1777102212
}
The exact payload fields must match the API reference and the values shown to the customer.
---
14. Step 3 — Sign transfer confirmation assertion
Use the device private key.
Protected header:
{
"alg": "RS256",
"kid": "device-key-001",
"typ": "JWT"
}
Signing process:
1. Build assertion payload
2. Canonicalise payload with JCS
3. base64url protected header
4. base64url canonical payload
5. Sign header.payload using RS256
6. Create compact JWS
---
15. Step 4 — Submit confirmation
POST https://api.yes.cash/v1/core/transfers/{transferId}/confirm
Authorization: Bearer <access-token>
Ocp-Apim-Subscription-Key: <subscription-key>
X-Correlation-Id: <uuid>
Idempotency-Key: <uuid>
Content-Type: application/json
Example request shape:
{
"deviceAssertion": "<compact-jws>"
}
Example response shape:
{
"transferId": "TRF-01HX9F2J7K3M5N7P9Q1R3T5V7W",
"transferStatus": "CONFIRMED",
"nextStep": "OPEN_FUNDING_WEBVIEW",
"fundingSessionId": "FND-01HX9F2J7K3M5N7P9Q1R3T5V7W",
"fundingWebviewUrl": "https://api.yes.cash/v1/core/funding-webview/FND-..."
}
Partner app action:
- open the funding webview if returned;
- otherwise poll transfer detail;
- show the next customer action.
---
16. Challenge expiry handling
Challenges are short-lived.
If the challenge expires:
- do not reuse the old assertion;
- fetch the latest transfer detail;
- rebuild the assertion with the latest challenge;
- ask the customer to confirm again if required.
Possible error:
device.challengeExpired
Partner app action:
GET /v1/core/transfers/{transferId}
→ rebuild assertion
→ POST /v1/core/transfers/{transferId}/confirm
---
17. Device not registered handling
If the API returns a device-registration-required error:
device.registrationRequired
Partner app action:
1. start device registration;
2. complete device registration;
3. return customer to the interrupted flow;
4. retry the transfer confirmation if still valid.
---
18. Device assertion failure handling
Possible causes:
| Cause | Partner app action |
|---|---|
| Wrong key used | Use the registered device key |
| Payload changed after signing | Rebuild and re-sign assertion |
| Challenge expired | Fetch latest transfer detail |
| Invalid canonicalisation | Fix JCS implementation |
| Invalid algorithm | Use RS256 |
| Private key unavailable | Start device recovery/re-registration |
| Transfer state changed | Fetch latest transfer detail |
Do not repeatedly retry a failed assertion without rebuilding the payload.
---
19. Device replacement flow
Use this when a customer changes device.
High-level flow:
Customer logs in on new device
→ App detects no active local device key
→ Start device registration
→ Complete required step-up if requested
→ Register new device key
→ Mark new device active locally
Partner app behavior:
- do not copy keys from the old device;
- generate a new key on the new device;
- follow any step-up or recovery instructions returned by the API;
- clear stale local device registration state if the API rejects it.
---
20. Lost device handling
If the customer loses the registered device:
- do not attempt to recover the old private key;
- start the approved recovery or new-device registration flow;
- follow the API response instructions;
- require the customer to complete any additional verification required by the flow.
Do not provide a partner-only bypass for transfer confirmation.
---
21. Local device state
The partner app may store local device state such as:
{
"deviceId": "DEV-01HX9F2J7K3M5N7P9Q1R3T5V7W",
"deviceKeyId": "device-key-001",
"registered": true,
"registeredAt": "2026-05-11T14:31:12Z"
}
Do not treat local state as authoritative.
If the API says the device is not registered or not active, follow the API response.
---
22. Customer UX guidance
After registration
Show:
This device is secured and ready to confirm transfers.
Before transfer confirmation
Show the exact transfer details:
- amount sent;
- amount received;
- recipient;
- fees;
- exchange rate;
- destination country;
- payout method.
Ask for explicit customer confirmation before signing.
If device registration is required
Show:
Please secure this device before confirming your transfer.
If confirmation fails
Show:
We could not confirm this transfer. Please try again.
If repeated failure occurs, guide the customer to support or device recovery.
---
23. Security checklist
The partner app must:
- generate keys on the device;
- use RSA-2048 or stronger;
- store the private key in secure storage;
- prevent private key export where supported;
- never send the private key to any server;
- never log private keys;
- sign only after showing the final transfer details;
- bind the assertion to the latest challenge;
- reject expired challenges;
- clear local device state on logout only if required by app policy;
- remove local key material on app uninstall where platform behavior allows.
---
24. Common implementation mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Signing with ES256 | API rejects assertion | Use RS256 |
| Using RSA key smaller than 2048 bits | API rejects key | Generate RSA-2048 or RSA-3072 |
| Sending private key to backend | Security failure | Keep private key on device |
| Signing before showing final transfer details | Weak customer confirmation | Show final details first |
| Reusing expired challenge | Confirmation fails | Fetch latest transfer detail |
| Payload field mismatch | Signature verifies but semantic validation fails | Bind exact API-required values |
| Pretty JSON signed instead of JCS | Signature verification fails | Canonicalise with JCS |
Missing kid | Key selection fails | Include key ID in JWS header |
---
25. Support information to provide
When escalating device authentication issues, provide:
- endpoint;
- timestamp;
- correlation ID;
- transfer ID, if relevant;
- device ID, if returned;
- error code;
- JWS header;
- sanitized JWS payload;
- whether key is hardware-backed, if known;
- device platform and OS version;
- app version.
Never send:
- device private key;
- customer password;
- OTP;
- access token;
- refresh token.
---
26. Certification checklist
Before go-live, confirm:
- device key generation works on supported platforms;
- private key is non-exportable where supported;
- public key registration succeeds;
- registration proof JWS verifies locally;
- transfer assertion JWS verifies locally;
kidis present in JWS headers;- RS256 is used everywhere;
- RSA-2048 minimum is enforced;
- JCS canonicalisation is deterministic;
- expired challenge handling works;
- device-not-registered recovery works;
- transfer confirmation happy path works;
- repeated assertion failure is handled safely;
- logs do not contain private keys or full tokens.