Quote Signing
Partner quote commitment, JWS signing, JCS canonicalisation, and validation rules.
Quote Signing Guide
This guide explains how a partner system should create and sign quote commitments before the customer accepts a transfer.
Use this host in all API examples:
https://api.yes.cash
---
1. Purpose
A signed quote is the partner's cryptographic commitment to the pricing shown to the customer.
The signed quote binds:
- customer subscription reference;
- send amount and currency;
- receive amount and currency;
- FX rate;
- partner fee;
- principal fee;
- total customer cost;
- corridor;
- quote validity window;
- partner quote sequence;
- partner signing key.
The customer must see the quote and disclosure before acceptance.
---
2. What the partner must build
The partner must implement:
1. a pricing engine;
2. a quote payload builder;
3. JSON canonicalisation using JCS;
4. JWS signing using RS256;
5. JWKS publication for the public signing keys;
6. quote storage and traceability;
7. quote expiry handling;
8. sequence number management.
---
3. Required signing standard
| Item | Requirement |
|---|---|
| JWS format | Compact JWS |
| Algorithm | RS256 |
| Key type | RSA |
| Minimum key size | RSA-2048 |
| Recommended key size | RSA-3072 |
| Payload canonicalisation | JCS |
| Encoding | base64url |
| Signature input | <base64url(header)>.<base64url(canonicalPayload)> |
| Public key format | JWKS |
| Key selection | kid in JWS protected header |
---
4. JWS compact format
A signed quote uses standard compact JWS format:
<base64url(header)>.<base64url(payload)>.<base64url(signature)>
Example placeholder:
eyJhbGciOiJSUzI1NiIsImtpZCI6InByLWtleS0wMSIsInR5cCI6IkpXVCJ9.eyJ...payload...fQ.SIGNATURE_BYTES
---
5. Protected header
Use this header shape:
{
"alg": "RS256",
"kid": "pr-key-01",
"typ": "JWT"
}
Header fields
| Field | Required | Description |
|---|---|---|
alg | Yes | Must be RS256 |
kid | Yes | Partner signing key identifier, matching the partner JWKS |
typ | Recommended | Use JWT |
Do not include
Do not include these header fields:
crit
x5c
x5t
x5u
jku
jwk
The API expects a closed, simple header.
---
6. Quote payload
The signed payload is a JSON object.
The payload must be canonicalised using JCS before signing.
Example payload
{
"beneficiary_country": "MA",
"beneficiary_currency": "MAD",
"corridor": "ES-MA",
"corridor_type": "OPEN_PAYMENT",
"exp": 1777105812,
"expires_at": "2026-05-11T15:30:12Z",
"fx_rate": "10.8500",
"iat": 1777102212,
"issued_at": "2026-05-11T14:30:12Z",
"jti": "01HX9F2J7K3M5N7P9Q1R3T5V88",
"partner_fee": "1.50",
"partner_id": "PRT-YESCASH-EU-DEMO-PARTNER",
"partner_quote_seq": 123,
"principal_fee": "1.00",
"quote_id": "QT-PARTNER-2026-05-11-0000000123",
"quote_signature_v1": "v1",
"receive_amount": "1085.00",
"receive_currency": "MAD",
"send_amount": "100.00",
"send_currency": "EUR",
"subscription_id": "SUB-01HX9F2J7K3M5N7P9Q1R3T5V7W",
"total_consumer_cost": "102.50"
}
Important naming rule:
- the JWS payload uses the exact claim names shown above;
- do not rename
subscription_idtosubscriptionIdinside the JWS payload; - API JSON responses may use camelCase fields, but the signed payload must follow the quote signing schema.
---
7. Payload field reference
| Claim | Type | Required | Description |
|---|---|---|---|
quote_signature_v1 | string | Yes | Scheme version. Use v1 |
quote_id | string | Yes | Partner-allocated unique quote ID |
jti | string | Yes | Globally unique quote issuance ID |
partner_id | string | Yes | Partner ID issued during onboarding |
partner_quote_seq | integer | Yes | Partner-managed monotonic sequence number |
subscription_id | string | Yes | Customer subscription reference used for quote binding |
send_amount | decimal string | Yes | Amount the customer sends |
send_currency | string | Yes | Origin currency, for example EUR |
receive_amount | decimal string | Yes | Amount recipient receives |
receive_currency | string | Yes | Destination currency, for example MAD |
beneficiary_country | string | Yes | Destination country code, for example MA |
beneficiary_currency | string | Yes | Destination currency code |
corridor | string | Yes | Corridor code, for example ES-MA |
corridor_type | string | Yes | Usually OPEN_PAYMENT or OFFLINE |
fx_rate | decimal string | Yes | FX rate shown to customer |
partner_fee | decimal string | Yes | Partner fee |
principal_fee | decimal string | Yes | YesCash fee |
total_consumer_cost | decimal string | Yes | Total amount/cost shown to customer |
iat | integer | Yes | Issued-at time as epoch seconds |
issued_at | RFC 3339 string | Yes | Human-readable issued-at time |
exp | integer | Yes | Expiry time as epoch seconds |
expires_at | RFC 3339 string | Yes | Human-readable expiry time |
---
8. Money field rules
Use strings for money values.
Correct:
{
"send_amount": "100.00",
"fx_rate": "10.8500",
"partner_fee": "1.50"
}
Avoid raw floating-point numbers:
{
"send_amount": 100.0,
"fx_rate": 10.85
}
Partner systems should use decimal-safe types, not binary floating point.
---
9. Time field rules
Each quote includes both machine and readable time fields.
| Field | Format | Example |
|---|---|---|
iat | epoch seconds | 1777102212 |
issued_at | RFC 3339 UTC | 2026-05-11T14:30:12Z |
exp | epoch seconds | 1777105812 |
expires_at | RFC 3339 UTC | 2026-05-11T15:30:12Z |
The partner app should not allow a customer to accept an expired quote.
If the API returns quote.expired, create a new quote and show the customer the updated pricing.
---
10. Sequence number rules
partner_quote_seq is a partner-managed integer sequence.
Recommended rules:
- allocate one sequence per partner signing system;
- make it strictly increasing;
- do not reuse sequence numbers;
- persist the sequence before returning the quote to the app;
- monitor sequence gaps.
Example:
123
124
125
126
---
11. JCS canonicalisation
Before signing, canonicalise the JSON payload using JCS.
JCS canonicalisation means:
- keys sorted lexicographically;
- UTF-8 encoding;
- no extra whitespace;
- deterministic JSON representation;
- same input produces the same byte sequence.
Pretty-printed JSON is only for humans. The signed payload must be the canonical byte representation.
Example canonical form
Pretty JSON:
{
"quote_signature_v1": "v1",
"quote_id": "QT-123",
"send_amount": "100.00"
}
Canonical form:
{"quote_id":"QT-123","quote_signature_v1":"v1","send_amount":"100.00"}
---
12. Signing process
The signing process is:
1. Build quote payload JSON
2. Validate all required claims
3. Canonicalise payload using JCS
4. Build protected header
5. base64url(header)
6. base64url(canonical payload)
7. Create signing input:
<base64url(header)>.<base64url(canonical payload)>
8. Sign using RSA private key with RS256
9. base64url(signature)
10. Return compact JWS:
header.payload.signature
---
13. Pseudocode
payload = buildQuotePayload()
validate(payload)
canonicalPayloadBytes = jcsCanonicalize(payload)
header = {
"alg": "RS256",
"kid": activeSigningKeyId,
"typ": "JWT"
}
encodedHeader = base64url(json(header))
encodedPayload = base64url(canonicalPayloadBytes)
signingInput = encodedHeader + "." + encodedPayload
signatureBytes = rsaSha256Sign(privateKey, ascii(signingInput))
encodedSignature = base64url(signatureBytes)
signedQuoteJws = signingInput + "." + encodedSignature
---
14. Where the signed quote is used
The exact API surface depends on the quote integration flow agreed during onboarding.
Common usage:
1. partner creates and signs the quote;
2. quote is made available to the YesCash API according to the integration contract;
3. customer fetches quote/disclosure;
4. customer accepts the quote;
5. API verifies the signed quote;
6. API rejects acceptance if the signature, binding, expiry, or values are invalid.
Customer-facing endpoints involved in the flow:
GET /v1/core/quotes/{quoteId}
GET /v1/core/quotes/{quoteId}/disclosure
POST /v1/core/quotes/{quoteId}/accept
---
15. Partner JWKS requirement
The partner must publish a JWKS containing the public key for each active signing key.
The JWS kid must match a key in the partner JWKS.
Example JWKS shape:
{
"keys": [
{
"kty": "RSA",
"kid": "pr-key-01",
"use": "sig",
"alg": "RS256",
"n": "<modulus>",
"e": "AQAB"
}
]
}
See 07_Partner_JWKS_Key_Management.md for full key management guidance.
---
16. Common validation failures
| Error | Likely cause | Partner action |
|---|---|---|
quote.signatureInvalid | Signature does not verify | Check canonicalisation, key, algorithm, payload bytes |
quote.expired | Quote expired before acceptance | Generate a new quote |
quote.bindingMismatch | Quote does not match customer subscription | Check subscription_id and authenticated customer session |
quote.sequenceGap | Sequence number issue | Check sequence generator and persistence |
quote.envelopeBreach | Pricing outside accepted limits | Recalculate pricing and regenerate quote |
quote.amountChanged | Accepted values differ from signed values | Rebuild quote with exact customer-visible values |
quote.invalid | Missing or malformed claim | Validate payload before signing |
Exact error names may vary by endpoint. Always check the API reference and sandbox behavior.
---
17. Implementation checklist
Before certification, confirm:
- quote payload uses exact required claim names;
quote_signature_v1is set tov1;subscription_idis included;- money values are decimal strings;
- timestamps are UTC;
iatmatchesissued_at;expmatchesexpires_at;partner_quote_seqis monotonic;- JCS canonicalisation is deterministic;
- header uses
alg: RS256; - header includes valid
kid; - private key is RSA-2048 or stronger;
- partner JWKS exposes the matching public key;
- generated JWS verifies locally before being sent;
- expired quotes are not shown as acceptable;
- quote acceptance uses the correct quote ID and customer session.
---
18. Local verification checklist
A partner should be able to verify a generated quote locally:
1. Split compact JWS into three parts.
2. Decode protected header.
3. Confirm alg = RS256.
4. Confirm kid exists in active JWKS.
5. Decode payload.
6. Re-canonicalise payload.
7. Rebuild signing input.
8. Verify signature using public key.
9. Confirm all required fields exist.
10. Confirm quote is not expired.
If local verification fails, the API verification will also fail.
---
19. Security rules
Partner systems must:
- protect private signing keys;
- restrict signing access to trusted backend systems;
- never sign quotes on the customer device;
- rotate keys according to the agreed key-management process;
- remove compromised keys from active use;
- keep signing logs for troubleshooting;
- never expose private keys in mobile apps, web apps, logs, or support tickets.
---
20. Support information to provide
When escalating quote signing issues, provide:
- environment;
- quote ID;
kid;- timestamp;
- correlation ID, if available;
- error code;
- unsigned payload, if safe to share;
- compact JWS, only through approved secure support channel;
- confirmation of canonicalisation library used;
- confirmation of signing algorithm.
Do not send private keys.