v1.0 draftDownloads
Guide

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

ItemRequirement
AlgorithmRS256
Key typeRSA
Minimum key sizeRSA-2048
Recommended key sizeRSA-3072
Payload canonicalisationJCS
Signature formatCompact JWS
Private key locationCustomer device secure storage
Public key locationRegistered 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:

PlatformRecommended storage
iOSKeychain / Secure Enclave where available
AndroidAndroid Keystore / hardware-backed key where available
WebWebAuthn/passkey-style secure authenticator where supported, or approved browser crypto model
DesktopOS 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 registrationId temporarily;
  • store registrationChallenge temporarily;
  • 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:

CausePartner app action
Wrong key usedUse the registered device key
Payload changed after signingRebuild and re-sign assertion
Challenge expiredFetch latest transfer detail
Invalid canonicalisationFix JCS implementation
Invalid algorithmUse RS256
Private key unavailableStart device recovery/re-registration
Transfer state changedFetch 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

MistakeImpactFix
Signing with ES256API rejects assertionUse RS256
Using RSA key smaller than 2048 bitsAPI rejects keyGenerate RSA-2048 or RSA-3072
Sending private key to backendSecurity failureKeep private key on device
Signing before showing final transfer detailsWeak customer confirmationShow final details first
Reusing expired challengeConfirmation failsFetch latest transfer detail
Payload field mismatchSignature verifies but semantic validation failsBind exact API-required values
Pretty JSON signed instead of JCSSignature verification failsCanonicalise with JCS
Missing kidKey selection failsInclude 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;
  • kid is 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.