v1.0 draftDownloads
Guide

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

ItemRequirement
JWS formatCompact JWS
AlgorithmRS256
Key typeRSA
Minimum key sizeRSA-2048
Recommended key sizeRSA-3072
Payload canonicalisationJCS
Encodingbase64url
Signature input<base64url(header)>.<base64url(canonicalPayload)>
Public key formatJWKS
Key selectionkid 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

FieldRequiredDescription
algYesMust be RS256
kidYesPartner signing key identifier, matching the partner JWKS
typRecommendedUse 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_id to subscriptionId inside the JWS payload;
  • API JSON responses may use camelCase fields, but the signed payload must follow the quote signing schema.

---

7. Payload field reference

ClaimTypeRequiredDescription
quote_signature_v1stringYesScheme version. Use v1
quote_idstringYesPartner-allocated unique quote ID
jtistringYesGlobally unique quote issuance ID
partner_idstringYesPartner ID issued during onboarding
partner_quote_seqintegerYesPartner-managed monotonic sequence number
subscription_idstringYesCustomer subscription reference used for quote binding
send_amountdecimal stringYesAmount the customer sends
send_currencystringYesOrigin currency, for example EUR
receive_amountdecimal stringYesAmount recipient receives
receive_currencystringYesDestination currency, for example MAD
beneficiary_countrystringYesDestination country code, for example MA
beneficiary_currencystringYesDestination currency code
corridorstringYesCorridor code, for example ES-MA
corridor_typestringYesUsually OPEN_PAYMENT or OFFLINE
fx_ratedecimal stringYesFX rate shown to customer
partner_feedecimal stringYesPartner fee
principal_feedecimal stringYesYesCash fee
total_consumer_costdecimal stringYesTotal amount/cost shown to customer
iatintegerYesIssued-at time as epoch seconds
issued_atRFC 3339 stringYesHuman-readable issued-at time
expintegerYesExpiry time as epoch seconds
expires_atRFC 3339 stringYesHuman-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.

FieldFormatExample
iatepoch seconds1777102212
issued_atRFC 3339 UTC2026-05-11T14:30:12Z
expepoch seconds1777105812
expires_atRFC 3339 UTC2026-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

ErrorLikely causePartner action
quote.signatureInvalidSignature does not verifyCheck canonicalisation, key, algorithm, payload bytes
quote.expiredQuote expired before acceptanceGenerate a new quote
quote.bindingMismatchQuote does not match customer subscriptionCheck subscription_id and authenticated customer session
quote.sequenceGapSequence number issueCheck sequence generator and persistence
quote.envelopeBreachPricing outside accepted limitsRecalculate pricing and regenerate quote
quote.amountChangedAccepted values differ from signed valuesRebuild quote with exact customer-visible values
quote.invalidMissing or malformed claimValidate 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_v1 is set to v1;
  • subscription_id is included;
  • money values are decimal strings;
  • timestamps are UTC;
  • iat matches issued_at;
  • exp matches expires_at;
  • partner_quote_seq is 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.