From f82d5092cdc8fa3a123404f680b43a937cc6ef11 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Sat, 31 Oct 2020 16:08:52 +0100 Subject: restructure merchant API documentation (#6492) --- core/api-common.rst | 9 + core/api-exchange.rst | 28 +- core/api-merchant.rst | 2374 ++++++++++++++++++++++++++----------------------- 3 files changed, 1289 insertions(+), 1122 deletions(-) (limited to 'core') diff --git a/core/api-common.rst b/core/api-common.rst index 1364ae5b..12f4a360 100644 --- a/core/api-common.rst +++ b/core/api-common.rst @@ -335,6 +335,15 @@ Blinded coin // Blinded coin's `public EdDSA key `, `base32` encoded type CoinEnvelope = string; +.. ts:def:: DenominationBlindingKeyP + + // Secret for blinding/unblinding. + // An RSA blinding secret, which is basically + // a 256-bit nonce, converted to Crockford `Base32`. + type DenominationBlindingKeyP = string; + + + .. _signature: Signatures diff --git a/core/api-exchange.rst b/core/api-exchange.rst index bc6a98d5..8b5c43cc 100644 --- a/core/api-exchange.rst +++ b/core/api-exchange.rst @@ -1435,21 +1435,21 @@ Refunds exchange_pub: EddsaPublicKey; } - .. ts:def:: RefundFailure + .. ts:def:: RefundFailure - interface RefundFailure { + interface RefundFailure { - // Numeric error code unique to the condition, which can be either - // related to the deposit value being insufficient for the requested - // refund(s), or the requested refund conflicting due to refund - // transaction number re-use (with different amounts). - code: number; + // Numeric error code unique to the condition, which can be either + // related to the deposit value being insufficient for the requested + // refund(s), or the requested refund conflicting due to refund + // transaction number re-use (with different amounts). + code: number; - // Human-readable description of the error message - hint: string; + // Human-readable description of the error message + hint: string; - // Information about the conflicting refund request(s). - // This will not be the full history of the coin, but only - // the relevant subset of the transactions. - history: CoinSpendHistoryItem[]; - } + // Information about the conflicting refund request(s). + // This will not be the full history of the coin, but only + // the relevant subset of the transactions. + history: CoinSpendHistoryItem[]; + } diff --git a/core/api-merchant.rst b/core/api-merchant.rst index 809bcbf1..53b64243 100644 --- a/core/api-merchant.rst +++ b/core/api-merchant.rst @@ -35,9 +35,12 @@ used. .. contents:: Table of Contents -------------------------- -Getting the configuration -------------------------- +----------------- +Configuration API +----------------- + +The configuration API exposes basic information about a merchant backend, +such as the implemented version of the protocol and the currency used. .. http:get:: /config @@ -63,1177 +66,1530 @@ Getting the configuration currency: string; } + +---------- +Wallet API +---------- --------------------------- -Dynamic Merchant Instances --------------------------- +This section describes (public) endpoints that wallets must be able +to interact with directly (without HTTP-based authentication). These +endpoints are used to process payments (claiming an order, paying +for the order, checking payment/refund status and aborting payments), +process refunds (check refund status, obtain refund), and to pickup +tips. -.. _instances: -.. http:get:: /private/instances - This is used to return the list of all the merchant instances +Claiming an order +----------------- - **Response:** +The first step of processing any Taler payment consists of the +(authorized) wallet claiming the order for itself. In this process, +the wallet provides a wallet-generated nonce that is added +into the contract terms. This step prevents two different +wallets from paying for the same contract, which would be bad +especially if the merchant only has finite stocks. - :status 200 OK: - The backend has successfully returned the list of instances stored. Returns - a `InstancesResponse`. +A claim token can be used to ensure that the wallet claiming an +order is actually authorized to do so. This is useful in cases +where order IDs are predictable and malicious actors may try to +claim orders (say in a case where stocks are limited). - .. ts:def:: InstancesResponse - interface InstancesResponse { - // List of instances that are present in the backend (see `Instance`) - instances: Instance[]; - } +.. http:post:: /orders/$ORDER_ID/claim - The `Instance` object describes the instance registered with the backend. - It does not include the full details, only those that usually concern the frontend. - It has the following structure: + Wallet claims ownership (via nonce) over an order. By claiming + an order, the wallet obtains the full contract terms, and thereby + implicitly also the hash of the contract terms it needs for the + other ``/public/`` APIs to authenticate itself as the wallet that + is indeed eligible to inspect this particular order's status. - .. ts:def:: Instance + **Request:** - interface Instance { - // Merchant name corresponding to this instance. - name: string; + The request must be a `ClaimRequest` - // Merchant instance this response is about ($INSTANCE) - id: string; + .. ts:def:: ClaimRequest - // Public key of the merchant/instance, in Crockford Base32 encoding. - merchant_pub: EddsaPublicKey; + interface ClaimRequest { + // Nonce to identify the wallet that claimed the order. + nonce: string; - // List of the payment targets supported by this instance. Clients can - // specify the desired payment target in /order requests. Note that - // front-ends do not have to support wallets selecting payment targets. - payment_targets: string[]; + // Token that authorizes the wallet to claim the order. + // *Optional* as the merchant may not have required it + // (``create_token`` set to ``false`` in `PostOrderRequest`). + token?: ClaimToken; + } - } + **Response:** + + :status 200 OK: + The client has successfully claimed the order. + The response contains the :ref:`contract terms `. + :status 404 Not found: + The backend is unaware of the instance or order. + :status 409 Conflict: + The someone else claimed the same order ID with different nonce before. + .. ts:def:: ClaimResponse -.. http:post:: /private/instances + interface ClaimResponse { + // Contract terms of the claimed order + contract_terms: ContractTerms; - This request will be used to create a new merchant instance in the backend. + // Signature by the merchant over the contract terms. + sig: EddsaSignature; + } + +Making the payment +------------------ + +.. http:post:: /orders/$ORDER_ID/pay + + Pay for an order by giving a deposit permission for coins. Typically used by + the customer's wallet. Note that this request does not include the + usual ``h_contract`` argument to authenticate the wallet, as the hash of + the contract is implied by the signatures of the coins. Furthermore, this + API doesn't really return useful information about the order. **Request:** - The request must be a `InstanceConfigurationMessage`. + The request must be a `pay request `. **Response:** - :status 204 No content: - The backend has successfully created the instance. + :status 200 OK: + The exchange accepted all of the coins. + The body is a `payment response `. + The ``frontend`` should now fulfill the contract. + Note that it is possible that refunds have been granted. + :status 400 Bad request: + Either the client request is malformed or some specific processing error + happened that may be the fault of the client as detailed in the JSON body + of the response. + :http:statuscode:`402 Payment required`: + There used to be a sufficient payment, but due to refunds the amount effectively + paid is no longer sufficient. (If the amount is generally insufficient, we + return "406 Not Acceptable", only if this is because of refunds we return 402.) + :status 403 Forbidden: + One of the coin signatures was not valid. + :status 404 Not found: + The merchant backend could not find the order or the instance and thus cannot process the payment. + :status 406 Not Acceptable: + The payment is insufficient (sum is below the required total amount). + :status 408 Request Timeout: + The backend took too long to process the request. Likely the merchant's connection + to the exchange timed out. Try again. :status 409 Conflict: - This instance already exists, but with other configuration options. - Use "PATCH" to update an instance configuration. + The exchange rejected the payment because a coin was already spent before. + The response will include the ``coin_pub`` for which the payment failed, + in addition to the response from the exchange to the ``/deposit`` request. + :status 410 Gone: + The offer has expired and is no longer available. + :status 412 Precondition Failed: + The given exchange is not acceptable for this merchant, as it is not in the + list of accepted exchanges and not audited by an approved auditor. + :status 424 Failed Dependency: + The merchant's interaction with the exchange failed in some way. + The client might want to try later again. + This includes failures like the denomination key of a coin not being + known to the exchange as far as the merchant can tell. - .. ts:def:: InstanceConfigurationMessage + The backend will return verbatim the error codes received from the exchange's + :ref:`deposit ` API. If the wallet made a mistake, like by + double-spending for example, the frontend should pass the reply verbatim to + the browser/wallet. If the payment was successful, the frontend MAY use + this to trigger some business logic. - interface InstanceConfigurationMessage { - // The URI where the wallet will send coins. A merchant may have - // multiple accounts, thus this is an array. Note that by - // removing URIs from this list the respective account is set to - // inactive and thus unavailable for new contracts, but preserved - // in the database as existing offers and contracts may still refer - // to it. - payto_uris: string[]; + .. ts:def:: PaymentResponse - // Name of the merchant instance to create (will become $INSTANCE). - id: string; + interface PaymentResponse { + // Signature on ``TALER_PaymentResponsePS`` with the public + // key of the merchant instance. + sig: EddsaSignature; - // Merchant name corresponding to this instance. - name: string; + } - // The merchant's physical address (to be put into contracts). - address: Location; + .. ts:def:: PayRequest - // The jurisdiction under which the merchant conducts its business - // (to be put into contracts). - jurisdiction: Location; + interface PayRequest { + // The coins used to make the payment. + coins: CoinPaySig[]; - // Maximum wire fee this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_wire_fee: Amount; + // The session for which the payment is made (or replayed). + // Only set for session-based payments. + session_id?: string; - // Default factor for wire fee amortization calculations. - // Can be overridden by the frontend on a per-order basis. - default_wire_fee_amortization: Integer; + } - // Maximum deposit fee (sum over all coins) this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_deposit_fee: Amount; + .. ts:def:: CoinPaySig - // If the frontend does NOT specify an execution date, how long should - // we tell the exchange to wait to aggregate transactions before - // executing the wire transfer? This delay is added to the current - // time when we generate the advisory execution time for the exchange. - default_wire_transfer_delay: RelativeTime; + export interface CoinPaySig { + // Signature by the coin. + coin_sig: EddsaSignature; - // If the frontend does NOT specify a payment deadline, how long should - // offers we make be valid by default? - default_pay_delay: RelativeTime; + // Public key of the coin being spend. + coin_pub: EddsaPublicKey; + + // Signature made by the denomination public key. + ub_sig: RsaSignature; + + // The hash of the denomination public key associated with this coin. + h_denom: HashCode; + + // The amount that is subtracted from this coin with this payment. + contribution: Amount; + // URL of the exchange this coin was withdrawn from. + exchange_url: string; } +Querying payment status +----------------------- -.. http:patch:: /private/instances/$INSTANCE +.. http:get:: /orders/$ORDER_ID - Update the configuration of a merchant instance. + Query the payment status of an order. This endpoint is for the wallet. + When the wallet goes to this URL and it is unpaid, + they will be prompted for payment. + This endpoint typically also supports requests with the "Accept" header + requesting "text/html". In this case, an HTML response suitable for + triggering the interaction with the wallet is returned, with ``timeout_ms`` + ignored (treated as zero). If the backend installation does not include the + required HTML templates, a 406 status code is returned. - **Request** + In the case that the request was made with a claim token (even the wrong one) + and the order was claimed and paid, the server will redirect the client to + the fulfillment URL. This redirection will happen with a 302 status code + if the "Accept" header specified "text/html", and with a 202 status code + otherwise. - The request must be a `InstanceReconfigurationMessage`. - Removing an existing payto_uri deactivates - the account (it will no longer be used for future contracts). + **Request:** + + :query h_contract=HASH: hash of the order's contract terms (this is used to authenticate the wallet/customer in case $ORDER_ID is guessable). Required once an order was claimed. + :query token=TOKEN: *Optional*. Authorizes the request via the claim token that was returned in the `PostOrderResponse`. Used with unclaimed orders only. Whether token authorization is required is determined by the merchant when the frontend creates the order. + :query session_id=STRING: *Optional*. Session ID that the payment must be bound to. If not specified, the payment is not session-bound. + :query timeout_ms=NUMBER: *Optional.* If specified, the merchant backend will + wait up to ``timeout_ms`` milliseconds for completion of the payment before + sending the HTTP response. A client must never rely on this behavior, as the + merchant backend may return a response immediately. + :query refund=AMOUNT: *Optional*. Indicates that we are polling for a refund above the given AMOUNT. Only useful in combination with timeout. + :query await_refund_obtained=BOOLEAN: *Optional*. If set to "yes", poll for the order's pending refunds to be picked up. **Response:** - :status 204 No content: - The backend has successfully created the instance. + :status 200 OK: + The response is a `StatusPaidResponse`. + :status 202 Accepted: + The response is a `StatusGotoResponse`. Only returned if the content type requested was not HTML. + :status 302 Found: + The client should go to the indicated location. Only returned if the content type requested was HTML. + :status 402 PaymentRequired: + The response is a `StatusUnpaidResponse`. + :status 403 Forbidden: + The ``h_contract`` (or the ``token`` for unclaimed orders) does not match the order + and we have no fulfillment URL in the contract. + :status 410 Gone: + The response is a `StatusGoneResponse`. :status 404 Not found: - This instance is unknown and thus cannot be reconfigured. + The merchant backend is unaware of the order. + :status 406 Not Acceptable: + The merchant backend could not load the template required to generate a reply in the desired format. (Likely HTML templates were not properly installed.) - .. ts:def:: InstanceReconfigurationMessage + .. ts:def:: StatusPaidResponse - interface InstanceReconfigurationMessage { - // The URI where the wallet will send coins. A merchant may have - // multiple accounts, thus this is an array. Note that by - // removing URIs from this list - payto_uris: string[]; + interface StatusPaid { + // Was the payment refunded (even partially, via refund or abort)? + refunded: boolean; - // Merchant name corresponding to this instance. - name: string; + // Is any amount of the refund still waiting to be picked up (even partially) + refund_pending: boolean; - // The merchant's physical address (to be put into contracts). - address: Location; + // Amount that was refunded in total. + refund_amount: Amount; + } - // The jurisdiction under which the merchant conducts its business - // (to be put into contracts). - jurisdiction: Location; - - // Maximum wire fee this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_wire_fee: Amount; + .. ts:def:: StatusGotoResponse - // Default factor for wire fee amortization calculations. - // Can be overridden by the frontend on a per-order basis. - default_wire_fee_amortization: Integer; + interface StatusGotoResponse { + // The client should go to the fulfillment URL, it may be ready or + // might have some other interesting status. + fulfillment_url: string; + } - // Maximum deposit fee (sum over all coins) this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_deposit_fee: Amount; + .. ts:def:: StatusUnpaidResponse - // If the frontend does NOT specify an execution date, how long should - // we tell the exchange to wait to aggregate transactions before - // executing the wire transfer? This delay is added to the current - // time when we generate the advisory execution time for the exchange. - default_wire_transfer_delay: RelativeTime; + interface StatusUnpaidResponse { + // URI that the wallet must process to complete the payment. + taler_pay_uri: string; - // If the frontend does NOT specify a payment deadline, how long should - // offers we make be valid by default? - default_pay_delay: RelativeTime; + // Status URL, can be used as a redirect target for the browser + // to show the order QR code / trigger the wallet. + fulfillment_url?: string; + // Alternative order ID which was paid for already in the same session. + // Only given if the same product was purchased before in the same session. + already_paid_order_id?: string; } + .. ts:def:: StatusGoneResponse -.. http:get:: /private/instances/$INSTANCE - - This is used to query a specific merchant instance. - - **Response:** + // The client tried to access the order via the claim + // token (and not a valid h_contract), but the order can't be claimed + // anymore, as it is already paid. + interface StatusGoneResponse { + // Fulfillment URL for the order. + fulfillment_url: string; + } - :status 200 OK: - The backend has successfully returned the list of instances stored. Returns - a `QueryInstancesResponse`. + +Demonstrating payment +--------------------- - .. ts:def:: QueryInstancesResponse +In case a wallet has already paid for an order, this is a fast way of proving +to the merchant that the order was already paid. The alternative would be to +replay the original payment, but simply providing the merchant's signature +saves bandwidth and computation time. - interface QueryInstancesResponse { - // The URI where the wallet will send coins. A merchant may have - // multiple accounts, thus this is an array. - accounts: MerchantAccount[]; +Demonstrating payment is useful in case a digital good was made available +only to clients with a particular session ID: if that session ID expired or +if the user is using a different client, demonstrating payment will allow +the user to regain access to the digital good without having to pay for it +again. - // Merchant name corresponding to this instance. - name: string; +.. http:post:: /orders/$ORDER_ID/paid - // Public key of the merchant/instance, in Crockford Base32 encoding. - merchant_pub: EddsaPublicKey; + Prove that the client previously paid for an order by providing + the merchant's signature from the `payment response `. + Typically used by the customer's wallet if it receives a request for + payment for an order that it already paid. This is more compact then + re-transmitting the full payment details. + Note that this request does include the + usual ``h_contract`` argument to authenticate the wallet and + to allow the merchant to verify the signature before checking + with its own database. - // The merchant's physical address (to be put into contracts). - address: Location; + **Request:** - // The jurisdiction under which the merchant conducts its business - // (to be put into contracts). - jurisdiction: Location; + The request must be a `paid request `. - // Maximum wire fee this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_wire_fee: Amount; + **Response:** - // Default factor for wire fee amortization calculations. - // Can be overridden by the frontend on a per-order basis. - default_wire_fee_amortization: Integer; + :status 204 No content: + The merchant accepted the signature. + The ``frontend`` should now fulfill the contract. + Note that it is possible that refunds have been granted. + :status 400 Bad request: + Either the client request is malformed or some specific processing error + happened that may be the fault of the client as detailed in the JSON body + of the response. + :status 403 Forbidden: + The signature was not valid. + :status 404 Not found: + The merchant backend could not find the order or the instance + and thus cannot process the request. + :status 409 Conflict: + The provided contract hash does not match this order. - // Maximum deposit fee (sum over all coins) this instance is willing to pay. - // Can be overridden by the frontend on a per-order basis. - default_max_deposit_fee: Amount; + .. ts:def:: PaidRequest - // If the frontend does NOT specify an execution date, how long should - // we tell the exchange to wait to aggregate transactions before - // executing the wire transfer? This delay is added to the current - // time when we generate the advisory execution time for the exchange. - default_wire_transfer_delay: RelativeTime; + interface PaidRequest { + // Signature on ``TALER_PaymentResponsePS`` with the public + // key of the merchant instance. + sig: EddsaSignature; - // If the frontend does NOT specify a payment deadline, how long should - // offers we make be valid by default? - default_pay_deadline: RelativeTime; + // hash of the order's contract terms (this is used to authenticate the + // wallet/customer and to enable signature verification without + // database access). + h_contract: HashCode; + // Session id for which the payment is proven. + session_id: string; } - .. ts:def:: MerchantAccount - - interface MerchantAccount { - - // payto:// URI of the account. - payto_uri: string; - - // Hash over the wire details (including over the salt) - h_wire: HashCode; - - // salt used to compute h_wire - salt: HashCode; - - // true if this account is active, - // false if it is historic. - active: boolean; - } + +Aborting incomplete payments +---------------------------- +In rare cases (such as a wallet restoring from an outdated backup) it is possible +that a wallet fails to complete a payment because it runs out of e-cash in the +middle of the process. The abort API allows the wallet to abort the payment for +such an incomplete payment and to regain control over the coins that were spent +so far. Aborts are not permitted for payments that completed. In contrast to +refunds, aborts do not require approval by the merchant because aborts always +are for incomplete payments for an order and never for established contracts. -.. http:delete:: /private/instances/$INSTANCE +.. _order-abort: +.. http:post:: /orders/$ORDER_ID/abort - This request will be used to delete (permanently disable) - or purge merchant instance in the backend. Purging will - delete all offers and payments associated with the instance, - while disabling (the default) only deletes the private key - and makes the instance unusable for new orders or payments. + Abort paying for an order and obtain a refund for coins that + were already deposited as part of a failed payment. **Request:** - :query purge: *Optional*. If set to YES, the instance will be fully - deleted. Otherwise only the private key would be deleted. + The request must be an `abort request `. We force the wallet + to specify the affected coins as it may only request for a subset of the coins + (i.e. because the wallet knows that some were double-spent causing the failure). + Also we need to know the coins because there may be two wallets "competing" over + the same order and one wants to abort while the other still proceeds with the + payment. Here we need to again know which subset of the deposits to abort. - **Response** + **Response:** - :status 204 No content: - The backend has successfully removed the instance. The body is empty. + :status 200 OK: + The merchant accepted the request, and passed it on to the exchange. The body is a + a `merchant refund response `. Note that the exchange + MAY still have encountered errors in processing. Those will then be part of + the body. Wallets MUST carefully consider errors for each of the coins as + returned by the exchange. + :status 400 Bad request: + Either the client request is malformed or some specific processing error + happened that may be the fault of the client as detailed in the JSON body + of the response. + :status 403 Forbidden: + The ``h_contract`` does not match the $ORDER_ID. :status 404 Not found: - The instance is unknown to the backend. - :status 409 Conflict: - The instance cannot be deleted because it has pending offers, or - the instance cannot be purged because it has successfully processed - payments that have not passed the TAX_RECORD_EXPIRATION time. - The latter case only applies if ``purge`` was set. - - --------------------- -Inventory management --------------------- + The merchant backend could not find the order or the instance + and thus cannot process the abort request. + :status 408 Request Timeout: + The merchant backend took too long getting a response from the exchange. + The wallet SHOULD retry soon. + :status 412 Precondition Failed: + Aborting the payment is not allowed, as the original payment did succeed. + It is possible that a different wallet succeeded with the payment. This + wallet should thus try to refresh all of the coins involved in the payment. + :status 424 Failed Dependency: + The merchant's interaction with the exchange failed in some way. + The error from the exchange is included. -.. _inventory: + The backend will return an `abort response `, which includes + verbatim the error codes received from the exchange's + :ref:`refund ` API. The frontend should pass the replies verbatim to + the browser/wallet. -Inventory management is an *optional* backend feature that can be used to -manage limited stocks of products and to auto-complete product descriptions in -contracts (such that the frontends have to do less work). You can use the -Taler merchant backend to process payments *without* using its inventory -management. + .. ts:def:: AbortRequest + interface AbortRequest { -.. http:get:: /private/products + // hash of the order's contract terms (this is used to authenticate the + // wallet/customer in case $ORDER_ID is guessable). + h_contract: HashCode; - This is used to return the list of all items in the inventory. + // List of coins the wallet would like to see refunds for. + // (Should be limited to the coins for which the original + // payment succeeded, as far as the wallet knows.) + coins: AbortingCoin[]; + } - **Response:** + .. ts:def:: AbortingCoin - :status 200 OK: - The backend has successfully returned the inventory. Returns - a `InventorySummaryResponse`. + interface AbortingCoin { + // Public key of a coin for which the wallet is requesting an abort-related refund. + coin_pub: EddsaPublicKey; - .. ts:def:: InventorySummaryResponse + // The amount to be refunded (matches the original contribution) + contribution: Amount; - interface InventorySummaryResponse { - // List of products that are present in the inventory - products: InventoryEntry[]; + // URL of the exchange this coin was withdrawn from. + exchange_url: string; } - The `InventoryEntry` object describes an item in the inventory. It has the following structure: - .. ts:def:: InventoryEntry + .. ts:def:: AbortResponse - interface InventoryEntry { - // Product identifier, as found in the product. - product_id: string; + interface AbortResponse { + // List of refund responses about the coins that the wallet + // requested an abort for. In the same order as the 'coins' + // from the original request. + // The rtransaction_id is implied to be 0. + refunds: MerchantAbortPayRefundStatus[]; } + .. ts:def:: MerchantAbortPayRefundStatus -.. http:get:: /private/products/$PRODUCT_ID - - This is used to obtain detailed information about a product in the inventory. + type MerchantAbortPayRefundStatus = + | MerchantAbortPayRefundSuccessStatus + | MerchantAbortPayRefundFailureStatus; - **Response:** + .. ts:def:: MerchantAbortPayRefundFailureStatus - :status 200 OK: - The backend has successfully returned the inventory. Returns - a `ProductDetail`. - - .. ts:def:: ProductDetail - - interface ProductDetail { - - // Human-readable product description. - description: string; + // Details about why a refund failed. + interface MerchantAbortPayRefundFailureStatus { + // Used as tag for the sum type RefundStatus sum type. + type: "failure" - // Map from IETF BCP 47 language tags to localized descriptions - description_i18n: { [lang_tag: string]: string }; + // HTTP status of the exchange request, must NOT be 200. + exchange_status: Integer; - // unit in which the product is measured (liters, kilograms, packages, etc.) - unit: string; + // Taler error code from the exchange reply, if available. + exchange_code?: Integer; - // The price for one ``unit`` of the product. Zero is used - // to imply that this product is not sold separately, or - // that the price is not fixed, and must be supplied by the - // front-end. If non-zero, this price MUST include applicable - // taxes. - price: Amount; + // If available, HTTP reply from the exchange. + exchange_reply?: Object; + } - // An optional base64-encoded product image - image: ImageDataUrl; + .. ts:def:: MerchantAbortPayRefundSuccessStatus - // a list of taxes paid by the merchant for one unit of this product - taxes: Tax[]; + // Additional details needed to verify the refund confirmation signature + // (``h_contract_terms`` and ``merchant_pub``) are already known + // to the wallet and thus not included. + interface MerchantAbortPayRefundSuccessStatus { + // Used as tag for the sum type MerchantCoinRefundStatus sum type. + type: "success" - // Number of units of the product in stock in sum in total, - // including all existing sales ever. Given in product-specific - // units. - // A value of -1 indicates "infinite" (i.e. for "electronic" books). - total_stock: Integer; + // HTTP status of the exchange request, 200 (integer) required for refund confirmations. + exchange_status: 200; - // Number of units of the product that have already been sold. - total_sold: Integer; + // the EdDSA :ref:`signature` (binary-only) with purpose + // `TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND` using a current signing key of the + // exchange affirming the successful refund + exchange_sig: EddsaSignature; - // Number of units of the product that were lost (spoiled, stolen, etc.) - total_lost: Integer; + // public EdDSA key of the exchange that was used to generate the signature. + // Should match one of the exchange's signing keys from /keys. It is given + // explicitly as the client might otherwise be confused by clock skew as to + // which signing key was used. + exchange_pub: EddsaPublicKey; + } - // Identifies where the product is in stock. - address: Location; - // Identifies when we expect the next restocking to happen. - next_restock?: Timestamp; +Obtaining refunds +----------------- - } +Refunds allow merchants to fully or partially restitute e-cash to a wallet, +for example because the merchant determined that it could not actually fulfill +the contract. Refunds must be approved by the merchant's business logic. -.. http:post:: /private/products +.. http:post:: /orders/$ORDER_ID/refund - This is used to add a product to the inventory. + Obtain refunds for an order. After talking to the exchange, the refunds will + no longer be pending if processed successfully. **Request:** - The request must be a `ProductAddDetail`. + The request body is a `WalletRefundRequest` object. **Response:** + :status 200 OK: + The response is a `WalletRefundResponse`. :status 204 No content: - The backend has successfully expanded the inventory. - :status 409 Conflict: - The backend already knows a product with this product ID, but with different details. + There are no refunds for the order. + :status 403 Forbidden: + The ``h_contract`` does not match the order. + :status 404 Not found: + The merchant backend is unaware of the order. + .. ts:def:: WalletRefundRequest - .. ts:def:: ProductAddDetail + interface WalletRefundRequest { + // hash of the order's contract terms (this is used to authenticate the + // wallet/customer). + h_contract: HashCode; + } - interface ProductAddDetail { + .. ts:def:: WalletRefundResponse - // product ID to use. - product_id: string; + interface WalletRefundResponse { + // Amount that was refunded in total. + refund_amount: Amount; - // Human-readable product description. - description: string; + // Successful refunds for this payment, empty array for none. + refunds: MerchantCoinRefundStatus[]; - // Map from IETF BCP 47 language tags to localized descriptions - description_i18n: { [lang_tag: string]: string }; + // Public key of the merchant. + merchant_pub: EddsaPublicKey; - // unit in which the product is measured (liters, kilograms, packages, etc.) - unit: string; + } - // The price for one ``unit`` of the product. Zero is used - // to imply that this product is not sold separately, or - // that the price is not fixed, and must be supplied by the - // front-end. If non-zero, this price MUST include applicable - // taxes. - price: Amount; + .. ts:def:: MerchantCoinRefundStatus - // An optional base64-encoded product image - image: ImageDataUrl; + type MerchantCoinRefundStatus = + | MerchantCoinRefundSuccessStatus + | MerchantCoinRefundFailureStatus; - // a list of taxes paid by the merchant for one unit of this product - taxes: Tax[]; + .. ts:def:: MerchantCoinRefundFailureStatus - // Number of units of the product in stock in sum in total, - // including all existing sales ever. Given in product-specific - // units. - // A value of -1 indicates "infinite" (i.e. for "electronic" books). - total_stock: Integer; + // Details about why a refund failed. + interface MerchantCoinRefundFailureStatus { + // Used as tag for the sum type RefundStatus sum type. + type: "failure"; - // Identifies where the product is in stock. - address: Location; + // HTTP status of the exchange request, must NOT be 200. + exchange_status: Integer; - // Identifies when we expect the next restocking to happen. - next_restock?: Timestamp; + // Taler error code from the exchange reply, if available. + exchange_code?: Integer; - } + // If available, HTTP reply from the exchange. + exchange_reply?: Object; + // Refund transaction ID. + rtransaction_id: Integer; + // public key of a coin that was refunded + coin_pub: EddsaPublicKey; -.. http:patch:: /private/products/$PRODUCT_ID + // Amount that was refunded, including refund fee charged by the exchange + // to the customer. + refund_amount: Amount; + } - This is used to update product details in the inventory. Note that the - ``total_stock`` and ``total_lost`` numbers MUST be greater or equal than - previous values (this design ensures idempotency). In case stocks were lost - but not sold, increment the ``total_lost`` number. All fields in the - request are optional, those that are not given are simply preserved (not - modified). Note that the ``description_i18n`` and ``taxes`` can only be - modified in bulk: if it is given, all translations must be provided, not - only those that changed. "never" should be used for the ``next_restock`` - timestamp to indicate no intention/possibility of restocking, while a time - of zero is used to indicate "unknown". + .. ts:def:: MerchantCoinRefundSuccessStatus - **Request:** + // Additional details needed to verify the refund confirmation signature + // (``h_contract_terms`` and ``merchant_pub``) are already known + // to the wallet and thus not included. + interface MerchantCoinRefundSuccessStatus { + // Used as tag for the sum type MerchantCoinRefundStatus sum type. + type: "success"; - The request must be a `ProductPatchDetail`. + // HTTP status of the exchange request, 200 (integer) required for refund confirmations. + exchange_status: 200; - **Response:** + // the EdDSA :ref:`signature` (binary-only) with purpose + // `TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND` using a current signing key of the + // exchange affirming the successful refund + exchange_sig: EddsaSignature; - :status 204 No content: - The backend has successfully expanded the inventory. + // public EdDSA key of the exchange that was used to generate the signature. + // Should match one of the exchange's signing keys from /keys. It is given + // explicitly as the client might otherwise be confused by clock skew as to + // which signing key was used. + exchange_pub: EddsaPublicKey; + // Refund transaction ID. + rtransaction_id: Integer; - .. ts:def:: ProductPatchDetail + // public key of a coin that was refunded + coin_pub: EddsaPublicKey; - interface ProductPatchDetail { + // Amount that was refunded, including refund fee charged by the exchange + // to the customer. + refund_amount: Amount; + } - // Human-readable product description. - description: string; - // Map from IETF BCP 47 language tags to localized descriptions - description_i18n: { [lang_tag: string]: string }; +Picking up tips +--------------- - // unit in which the product is measured (liters, kilograms, packages, etc.) - unit: string; +Tips are a way for wallets to obtain e-cash from +a website. - // The price for one ``unit`` of the product. Zero is used - // to imply that this product is not sold separately, or - // that the price is not fixed, and must be supplied by the - // front-end. If non-zero, this price MUST include applicable - // taxes. - price: Amount; +.. http:get:: /tips/$TIP_ID - // An optional base64-encoded product image - image: ImageDataUrl; + Handle request from wallet to provide details about a tip. - // a list of taxes paid by the merchant for one unit of this product - taxes: Tax[]; + This endpoint typically also supports requests with the "Accept" header + requesting "text/html". In this case, an HTML response suitable for + triggering the interaction with the wallet is returned. If the backend + installation does not include the required HTML templates, a 406 status + code is returned. - // Number of units of the product in stock in sum in total, - // including all existing sales ever. Given in product-specific - // units. - // A value of -1 indicates "infinite" (i.e. for "electronic" books). - total_stock: Integer; + **Response:** - // Number of units of the product that were lost (spoiled, stolen, etc.) - total_lost: Integer; + :status 200 OK: + A tip is being returned. The backend responds with a `TipInformation`. + :status 404 Not Found: + The tip identifier is unknown. + :status 406 Not Acceptable: + The merchant backend could not load the template required to generate a reply in the desired format. (Likely HTML templates were not properly installed.) + :status 410 Gone: + A tip has been fully claimed. The JSON reply still contains the `TipInformation`. - // Identifies where the product is in stock. - address: Location; + .. ts:def:: TipInformation - // Identifies when we expect the next restocking to happen. - next_restock?: Timestamp; + interface TipInformation { + // Exchange from which the tip will be withdrawn. Needed by the + // wallet to determine denominations, fees, etc. + exchange_url: string; + + // (remaining) amount of the tip (including fees). + tip_amount: Amount; + + // Timestamp indicating when the tip is set to expire (may be in the past). + // Note that tips that have expired MAY also result in a 404 response. + expiration: Timestamp; } +.. http:post:: /tips/$TIP_ID/pickup -.. http:post:: /private/products/$PRODUCT_ID/lock - - This is used to lock a certain quantity of the product for a limited - duration while the customer assembles a complete order. Note that - frontends do not have to "unlock", they may rely on the timeout as - given in the ``duration`` field. Re-posting a lock with a different - ``duration`` or ``quantity`` updates the existing lock for the same UUID - and does not result in a conflict. - - Unlocking by using a ``quantity`` of zero is is - optional but recommended if customers remove products from the - shopping cart. Note that actually POSTing to ``/orders`` with set - ``manage_inventory`` and using ``lock_uuid`` will **transition** the - lock to the newly created order (which may have a different ``duration`` - and ``quantity`` than what was requested in the lock operation). - If an order is for fewer items than originally locked, the difference - is automatically unlocked. + Handle request from wallet to pick up a tip. **Request:** - The request must be a `LockRequest`. + The request body is a `TipPickupRequest` object. **Response:** - :status 204 No content: - The backend has successfully locked (or unlocked) the requested ``quantity``. - :status 404 Not found: - The backend has does not know this product. + :status 200 OK: + A tip is being returned. The backend responds with a `TipResponse` + :status 401 Unauthorized: + The tip amount requested exceeds the tip. + :status 404 Not Found: + The tip identifier is unknown. + :status 409 Conflict: + Some of the denomination key hashes of the request do not match those currently available from the exchange (hence there is a conflict between what the wallet requests and what the merchant believes the exchange can provide). :status 410 Gone: - The backend does not have enough of product in stock. + The tip has expired. - .. ts:def:: LockRequest + .. ts:def:: TipPickupRequest - interface LockRequest { + interface TipPickupRequest { - // UUID that identifies the frontend performing the lock - lock_uuid: UUID; + // List of planches the wallet wants to use for the tip + planchets: PlanchetDetail[]; + } - // How long does the frontend intend to hold the lock - duration: RelativeTime; + .. ts:def:: PlanchetDetail - // How many units should be locked? - quantity: Integer; + interface PlanchetDetail { + // Hash of the denomination's public key (hashed to reduce + // bandwidth consumption) + denom_pub_hash: HashCode; + // coin's blinded public key + coin_ev: CoinEnvelope; } + .. ts:def:: TipResponse -.. http:delete:: /private/products/$PRODUCT_ID + interface TipResponse { - Delete information about a product. Fails if the product is locked by - anyone. + // Blind RSA signatures over the planchets. + // The order of the signatures matches the planchets list. + blind_sigs: BlindSignature[]; + } - **Response:** + .. ts:def:: BlindSignature - :status 204 No content: - The backend has successfully deleted the product. - :status 404 Not found: - The backend does not know the instance or the product. - :status 409 Conflict: - The backend refuses to delete the product because it is locked. + interface BlindSignature { + // The (blind) RSA signature. Still needs to be unblinded. + blind_sig: BlindedRsaSignature; + } ------------------- -Payment processing ------------------- -.. _post-order: +------------------- +Instance management +------------------- -.. http:post:: /private/orders +Instances allow one merchant backend to be shared by multiple merchants. +Every backend must have at least one instance, typcially the "default" +instance setup before it can be used to manage inventory or process payments. - Create a new order that a customer can pay for. - This request is **not** idempotent unless an ``order_id`` is explicitly specified. - However, while repeating without an ``order_id`` will create another order, that is - generally pretty harmless (as long as only one of the orders is returned to the wallet). +Setting up instances +-------------------- - .. note:: +.. http:post:: /private/instances - This endpoint does not return a URL to redirect your user to confirm the - payment. In order to get this URL use :http:get:/orders/$ORDER_ID. The - API is structured this way since the payment redirect URL is not unique - for every order, there might be varying parameters such as the session id. + This request will be used to create a new merchant instance in the backend. **Request:** - The request must be a `PostOrderRequest`. + The request must be a `InstanceConfigurationMessage`. **Response:** - :status 200 OK: - The backend has successfully created the proposal. The response is a - :ts:type:`PostOrderResponse`. - :status 404 Not found: - The order given used products from the inventory, but those were not found - in the inventory. Or the merchant instance is unknown (including possibly the instance being not configured for new orders). Details in the - error code. NOTE: no good way to find out which product is not in the - inventory, we MAY want to specify that in the reply. + :status 204 No content: + The backend has successfully created the instance. :status 409 Conflict: - A different proposal already exists under the specified order ID. - :status 410 Gone: - The order given used products from the inventory that are out of stock. - The response is a :ts:type:`OutOfStockResponse`. - - - .. ts:def:: PostOrderRequest - - interface PostOrderRequest { - // The order must at least contain the minimal - // order detail, but can override all - order: Order; - - // if set, the backend will then set the refund deadline to the current - // time plus the specified delay. If it's not set, refunds will not be - // possible. - refund_delay?: RelativeTime; + This instance already exists, but with other configuration options. + Use "PATCH" to update an instance configuration. - // specifies the payment target preferred by the client. Can be used - // to select among the various (active) wire methods supported by the instance. - payment_target?: string; + .. ts:def:: InstanceConfigurationMessage - // specifies that some products are to be included in the - // order from the inventory. For these inventory management - // is performed (so the products must be in stock) and - // details are completed from the product data of the backend. - inventory_products?: MinimalInventoryProduct[]; + interface InstanceConfigurationMessage { + // The URI where the wallet will send coins. A merchant may have + // multiple accounts, thus this is an array. Note that by + // removing URIs from this list the respective account is set to + // inactive and thus unavailable for new contracts, but preserved + // in the database as existing offers and contracts may still refer + // to it. + payto_uris: string[]; - // Specifies a lock identifier that was used to - // lock a product in the inventory. Only useful if - // ``manage_inventory`` is set. Used in case a frontend - // reserved quantities of the individual products while - // the shopping card was being built. Multiple UUIDs can - // be used in case different UUIDs were used for different - // products (i.e. in case the user started with multiple - // shopping sessions that were combined during checkout). - lock_uuids?: UUID[]; + // Name of the merchant instance to create (will become $INSTANCE). + id: string; - // Should a token for claiming the order be generated? - // False can make sense if the ORDER_ID is sufficiently - // high entropy to prevent adversarial claims (like it is - // if the backend auto-generates one). Default is 'true'. - create_token?: boolean; + // Merchant name corresponding to this instance. + name: string; - } + // The merchant's physical address (to be put into contracts). + address: Location; - .. ts:def:: Order + // The jurisdiction under which the merchant conducts its business + // (to be put into contracts). + jurisdiction: Location; - type Order : MinimalOrderDetail | ContractTerms; + // Maximum wire fee this instance is willing to pay. + // Can be overridden by the frontend on a per-order basis. + default_max_wire_fee: Amount; - The following fields must be specified in the ``order`` field of the request. Other fields from - `ContractTerms` are optional, and will override the defaults in the merchant configuration. + // Default factor for wire fee amortization calculations. + // Can be overridden by the frontend on a per-order basis. + default_wire_fee_amortization: Integer; - .. ts:def:: MinimalOrderDetail + // Maximum deposit fee (sum over all coins) this instance is willing to pay. + // Can be overridden by the frontend on a per-order basis. + default_max_deposit_fee: Amount; - interface MinimalOrderDetail { - // Amount to be paid by the customer - amount: Amount; + // If the frontend does NOT specify an execution date, how long should + // we tell the exchange to wait to aggregate transactions before + // executing the wire transfer? This delay is added to the current + // time when we generate the advisory execution time for the exchange. + default_wire_transfer_delay: RelativeTime; - // Short summary of the order - summary: string; + // If the frontend does NOT specify a payment deadline, how long should + // offers we make be valid by default? + default_pay_delay: RelativeTime; - // URL that will show that the order was successful after - // it has been paid for. Optional. When POSTing to the - // merchant, the placeholder "${ORDER_ID}" will be - // replaced with the actual order ID (useful if the - // order ID is generated server-side and needs to be - // in the URL). - fulfillment_url?: string; } - The following fields can be specified if the order is inventory-based. - In this case, the backend can compute the amounts from the prices given - in the inventory. Note that if the frontend does give more details - (towards the ContractTerms), this will override those details - (including total price) that would otherwise computed based on information - from the inventory. - .. ts:def:: ProductSpecification +.. http:patch:: /private/instances/$INSTANCE - type ProductSpecification : (MinimalInventoryProduct | Product); + Update the configuration of a merchant instance. + **Request** - .. ts:def:: MinimalInventoryProduct + The request must be a `InstanceReconfigurationMessage`. + Removing an existing payto_uri deactivates + the account (it will no longer be used for future contracts). - Note that if the frontend does give details beyond these, - it will override those details (including price or taxes) - that the backend would otherwise fill in via the inventory. + **Response:** - interface MinimalInventoryProduct { - // Which product is requested (here mandatory!) - product_id: string; + :status 204 No content: + The backend has successfully created the instance. + :status 404 Not found: + This instance is unknown and thus cannot be reconfigured. - // How many units of the product are requested - quantity: Integer; - } + .. ts:def:: InstanceReconfigurationMessage + interface InstanceReconfigurationMessage { + // The URI where the wallet will send coins. A merchant may have + // multiple accounts, thus this is an array. Note that by + // removing URIs from this list + payto_uris: string[]; - .. ts:def:: PostOrderResponse + // Merchant name corresponding to this instance. + name: string; - interface PostOrderResponse { - // Order ID of the response that was just created - order_id: string; + // The merchant's physical address (to be put into contracts). + address: Location; - // Token that authorizes the wallet to claim the order. - // Provided only if "create_token" was set to 'true' - // in the request. - token?: ClaimToken; - } + // The jurisdiction under which the merchant conducts its business + // (to be put into contracts). + jurisdiction: Location; + // Maximum wire fee this instance is willing to pay. + // Can be overridden by the frontend on a per-order basis. + default_max_wire_fee: Amount; - .. ts:def:: OutOfStockResponse + // Default factor for wire fee amortization calculations. + // Can be overridden by the frontend on a per-order basis. + default_wire_fee_amortization: Integer; - interface OutOfStockResponse { - // Which items are out of stock? - missing_products: OutOfStockEntry; - } + // Maximum deposit fee (sum over all coins) this instance is willing to pay. + // Can be overridden by the frontend on a per-order basis. + default_max_deposit_fee: Amount; - .. ts:def:: OutOfStockEntry - - interface OutOfStockEntry { - // Product ID of an out-of-stock item - product_id: string; - - // Requested quantity - requested_quantity: Integer; + // If the frontend does NOT specify an execution date, how long should + // we tell the exchange to wait to aggregate transactions before + // executing the wire transfer? This delay is added to the current + // time when we generate the advisory execution time for the exchange. + default_wire_transfer_delay: RelativeTime; - // Available quantity (must be below ``requested_quanitity``) - available_quantity: Integer; + // If the frontend does NOT specify a payment deadline, how long should + // offers we make be valid by default? + default_pay_delay: RelativeTime; - // When do we expect the product to be again in stock? - // Optional, not given if unknown. - restock_expected?: Timestamp; } +Inspecting instances +-------------------- -.. http:get:: /private/orders - - Returns known orders up to some point in the past. - - **Request:** +.. _instances: +.. http:get:: /private/instances - :query paid: *Optional*. If set to yes, only return paid orders, if no only unpaid orders. Do not give (or use "all") to see all orders regardless of payment status. - :query refunded: *Optional*. If set to yes, only return refunded orders, if no only unrefunded orders. Do not give (or use "all") to see all orders regardless of refund status. - :query wired: *Optional*. If set to yes, only return wired orders, if no only orders with missing wire transfers. Do not give (or use "all") to see all orders regardless of wire transfer status. - :query date: *Optional.* Time threshold, see ``delta`` for its interpretation. Defaults to the oldest or most recent entry, depending on ``delta``. - :query start: *Optional*. Row number threshold, see ``delta`` for its interpretation. Defaults to ``UINT64_MAX``, namely the biggest row id possible in the database. - :query delta: *Optional*. takes value of the form ``N (-N)``, so that at most ``N`` values strictly older (younger) than ``start`` and ``date`` are returned. Defaults to ``-20`` to return the last 20 entries (before ``start`` and/or ``date``). - :query timeout_ms: *Optional*. Timeout in milli-seconds to wait for additional orders if the answer would otherwise be negative (long polling). Only useful if delta is positive. Note that the merchant MAY still return a response that contains fewer than delta orders. + This is used to return the list of all the merchant instances **Response:** :status 200 OK: - The response is an `OrderHistory`. + The backend has successfully returned the list of instances stored. Returns + a `InstancesResponse`. - .. ts:def:: OrderHistory + .. ts:def:: InstancesResponse - interface OrderHistory { - // timestamp-sorted array of all orders matching the query. - // The order of the sorting depends on the sign of ``delta``. - orders : OrderHistoryEntry[]; + interface InstancesResponse { + // List of instances that are present in the backend (see `Instance`) + instances: Instance[]; } + The `Instance` object describes the instance registered with the backend. + It does not include the full details, only those that usually concern the frontend. + It has the following structure: - .. ts:def:: OrderHistoryEntry + .. ts:def:: Instance - interface OrderHistoryEntry { + interface Instance { + // Merchant name corresponding to this instance. + name: string; - // order ID of the transaction related to this entry. - order_id: string; + // Merchant instance this response is about ($INSTANCE) + id: string; - // row ID of the order in the database - row_id: number; + // Public key of the merchant/instance, in Crockford Base32 encoding. + merchant_pub: EddsaPublicKey; - // when the order was created - timestamp: Timestamp; + // List of the payment targets supported by this instance. Clients can + // specify the desired payment target in /order requests. Note that + // front-ends do not have to support wallets selecting payment targets. + payment_targets: string[]; - // the amount of money the order is for - amount: Amount; + } - // the summary of the order - summary: string; - // whether some part of the order is refundable, - // that is the refund deadline has not yet expired - // and the total amount refunded so far is below - // the value of the original transaction. - refundable: boolean; +.. http:get:: /private/instances/$INSTANCE - // whether the order has been paid or not - paid: boolean; - } + This is used to query a specific merchant instance. + **Response:** + :status 200 OK: + The backend has successfully returned the list of instances stored. Returns + a `QueryInstancesResponse`. -.. http:post:: /orders/$ORDER_ID/claim + .. ts:def:: QueryInstancesResponse - Wallet claims ownership (via nonce) over an order. By claiming - an order, the wallet obtains the full contract terms, and thereby - implicitly also the hash of the contract terms it needs for the - other ``/public/`` APIs to authenticate itself as the wallet that - is indeed eligible to inspect this particular order's status. + interface QueryInstancesResponse { + // The URI where the wallet will send coins. A merchant may have + // multiple accounts, thus this is an array. + accounts: MerchantAccount[]; - **Request:** + // Merchant name corresponding to this instance. + name: string; - The request must be a `ClaimRequest` + // Public key of the merchant/instance, in Crockford Base32 encoding. + merchant_pub: EddsaPublicKey; - .. ts:def:: ClaimRequest + // The merchant's physical address (to be put into contracts). + address: Location; - interface ClaimRequest { - // Nonce to identify the wallet that claimed the order. - nonce: string; + // The jurisdiction under which the merchant conducts its business + // (to be put into contracts). + jurisdiction: Location; + + // Maximum wire fee this instance is willing to pay. + // Can be overridden by the frontend on a per-order basis. + default_max_wire_fee: Amount; + + // Default factor for wire fee amortization calculations. + // Can be overridden by the frontend on a per-order basis. + default_wire_fee_amortization: Integer; + + // Maximum deposit fee (sum over all coins) this instance is willing to pay. + // Can be overridden by the frontend on a per-order basis. + default_max_deposit_fee: Amount; + + // If the frontend does NOT specify an execution date, how long should + // we tell the exchange to wait to aggregate transactions before + // executing the wire transfer? This delay is added to the current + // time when we generate the advisory execution time for the exchange. + default_wire_transfer_delay: RelativeTime; + + // If the frontend does NOT specify a payment deadline, how long should + // offers we make be valid by default? + default_pay_deadline: RelativeTime; - // Token that authorizes the wallet to claim the order. - // *Optional* as the merchant may not have required it - // (``create_token`` set to ``false`` in `PostOrderRequest`). - token?: ClaimToken; } - **Response:** + .. ts:def:: MerchantAccount - :status 200 OK: - The client has successfully claimed the order. - The response contains the :ref:`contract terms `. - :status 404 Not found: - The backend is unaware of the instance or order. - :status 409 Conflict: - The someone else claimed the same order ID with different nonce before. + interface MerchantAccount { - .. ts:def:: ClaimResponse + // payto:// URI of the account. + payto_uri: string; - interface ClaimResponse { - // Contract terms of the claimed order - contract_terms: ContractTerms; + // Hash over the wire details (including over the salt) + h_wire: HashCode; - // Signature by the merchant over the contract terms. - sig: EddsaSignature; + // salt used to compute h_wire + salt: HashCode; + + // true if this account is active, + // false if it is historic. + active: boolean; } + +Deleting instances +------------------ -.. http:post:: /orders/$ORDER_ID/pay +.. http:delete:: /private/instances/$INSTANCE - Pay for an order by giving a deposit permission for coins. Typically used by - the customer's wallet. Note that this request does not include the - usual ``h_contract`` argument to authenticate the wallet, as the hash of - the contract is implied by the signatures of the coins. Furthermore, this - API doesn't really return useful information about the order. + This request will be used to delete (permanently disable) + or purge merchant instance in the backend. Purging will + delete all offers and payments associated with the instance, + while disabling (the default) only deletes the private key + and makes the instance unusable for new orders or payments. **Request:** - The request must be a `pay request `. + :query purge: *Optional*. If set to YES, the instance will be fully + deleted. Otherwise only the private key would be deleted. - **Response:** + **Response** - :status 200 OK: - The exchange accepted all of the coins. - The body is a `payment response `. - The ``frontend`` should now fulfill the contract. - Note that it is possible that refunds have been granted. - :status 400 Bad request: - Either the client request is malformed or some specific processing error - happened that may be the fault of the client as detailed in the JSON body - of the response. - :status 402 Payment required: - There used to be a sufficient payment, but due to refunds the amount effectively - paid is no longer sufficient. (If the amount is generally insufficient, we - return "406 Not Acceptable", only if this is because of refunds we return 402.) - :status 403 Forbidden: - One of the coin signatures was not valid. + :status 204 No content: + The backend has successfully removed the instance. The body is empty. :status 404 Not found: - The merchant backend could not find the order or the instance and thus cannot process the payment. - :status 406 Not Acceptable: - The payment is insufficient (sum is below the required total amount). - :status 408 Request Timeout: - The backend took too long to process the request. Likely the merchant's connection - to the exchange timed out. Try again. + The instance is unknown to the backend. :status 409 Conflict: - The exchange rejected the payment because a coin was already spent before. - The response will include the ``coin_pub`` for which the payment failed, - in addition to the response from the exchange to the ``/deposit`` request. - :status 410 Gone: - The offer has expired and is no longer available. - :status 412 Precondition Failed: - The given exchange is not acceptable for this merchant, as it is not in the - list of accepted exchanges and not audited by an approved auditor. - :status 424 Failed Dependency: - The merchant's interaction with the exchange failed in some way. - The client might want to try later again. - This includes failures like the denomination key of a coin not being - known to the exchange as far as the merchant can tell. + The instance cannot be deleted because it has pending offers, or + the instance cannot be purged because it has successfully processed + payments that have not passed the TAX_RECORD_EXPIRATION time. + The latter case only applies if ``purge`` was set. - The backend will return verbatim the error codes received from the exchange's - :ref:`deposit ` API. If the wallet made a mistake, like by - double-spending for example, the frontend should pass the reply verbatim to - the browser/wallet. If the payment was successful, the frontend MAY use - this to trigger some business logic. - .. ts:def:: PaymentResponse +-------------------- +Inventory management +-------------------- - interface PaymentResponse { - // Signature on ``TALER_PaymentResponsePS`` with the public - // key of the merchant instance. - sig: EddsaSignature; +.. _inventory: - } +Inventory management is an *optional* backend feature that can be used to +manage limited stocks of products and to auto-complete product descriptions in +contracts (such that the frontends have to do less work). You can use the +Taler merchant backend to process payments *without* using its inventory +management. - .. ts:def:: PayRequest - interface PayRequest { - // The coins used to make the payment. - coins: CoinPaySig[]; +Adding products to the inventory +-------------------------------- - // The session for which the payment is made (or replayed). - // Only set for session-based payments. - session_id?: string; +.. http:post:: /private/products - } + This is used to add a product to the inventory. - .. ts:def:: CoinPaySig + **Request:** - export interface CoinPaySig { - // Signature by the coin. - coin_sig: EddsaSignature; + The request must be a `ProductAddDetail`. - // Public key of the coin being spend. - coin_pub: EddsaPublicKey; + **Response:** - // Signature made by the denomination public key. - ub_sig: RsaSignature; + :status 204 No content: + The backend has successfully expanded the inventory. + :status 409 Conflict: + The backend already knows a product with this product ID, but with different details. - // The hash of the denomination public key associated with this coin. - h_denom: HashCode; - // The amount that is subtracted from this coin with this payment. - contribution: Amount; + .. ts:def:: ProductAddDetail + + interface ProductAddDetail { + + // product ID to use. + product_id: string; + + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one ``unit`` of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; - // URL of the exchange this coin was withdrawn from. - exchange_url: string; } -.. http:post:: /orders/$ORDER_ID/paid - Prove that the client previously paid for an order by providing - the merchant's signature from the `payment response `. - Typically used by the customer's wallet if it receives a request for - payment for an order that it already paid. This is more compact then - re-transmitting the full payment details. - Note that this request does include the - usual ``h_contract`` argument to authenticate the wallet and - to allow the merchant to verify the signature before checking - with its own database. +.. http:patch:: /private/products/$PRODUCT_ID + + This is used to update product details in the inventory. Note that the + ``total_stock`` and ``total_lost`` numbers MUST be greater or equal than + previous values (this design ensures idempotency). In case stocks were lost + but not sold, increment the ``total_lost`` number. All fields in the + request are optional, those that are not given are simply preserved (not + modified). Note that the ``description_i18n`` and ``taxes`` can only be + modified in bulk: if it is given, all translations must be provided, not + only those that changed. "never" should be used for the ``next_restock`` + timestamp to indicate no intention/possibility of restocking, while a time + of zero is used to indicate "unknown". **Request:** - The request must be a `paid request `. + The request must be a `ProductPatchDetail`. **Response:** :status 204 No content: - The merchant accepted the signature. - The ``frontend`` should now fulfill the contract. - Note that it is possible that refunds have been granted. - :status 400 Bad request: - Either the client request is malformed or some specific processing error - happened that may be the fault of the client as detailed in the JSON body - of the response. - :status 403 Forbidden: - The signature was not valid. + The backend has successfully expanded the inventory. + + + .. ts:def:: ProductPatchDetail + + interface ProductPatchDetail { + + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one ``unit`` of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Number of units of the product that were lost (spoiled, stolen, etc.) + total_lost: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; + + } + +Inspecting inventory +-------------------- + +.. http:get:: /private/products + + This is used to return the list of all items in the inventory. + + **Response:** + + :status 200 OK: + The backend has successfully returned the inventory. Returns + a `InventorySummaryResponse`. + + .. ts:def:: InventorySummaryResponse + + interface InventorySummaryResponse { + // List of products that are present in the inventory + products: InventoryEntry[]; + } + + The `InventoryEntry` object describes an item in the inventory. It has the following structure: + + .. ts:def:: InventoryEntry + + interface InventoryEntry { + // Product identifier, as found in the product. + product_id: string; + + } + + +.. http:get:: /private/products/$PRODUCT_ID + + This is used to obtain detailed information about a product in the inventory. + + **Response:** + + :status 200 OK: + The backend has successfully returned the inventory. Returns + a `ProductDetail`. + + .. ts:def:: ProductDetail + + interface ProductDetail { + + // Human-readable product description. + description: string; + + // Map from IETF BCP 47 language tags to localized descriptions + description_i18n: { [lang_tag: string]: string }; + + // unit in which the product is measured (liters, kilograms, packages, etc.) + unit: string; + + // The price for one ``unit`` of the product. Zero is used + // to imply that this product is not sold separately, or + // that the price is not fixed, and must be supplied by the + // front-end. If non-zero, this price MUST include applicable + // taxes. + price: Amount; + + // An optional base64-encoded product image + image: ImageDataUrl; + + // a list of taxes paid by the merchant for one unit of this product + taxes: Tax[]; + + // Number of units of the product in stock in sum in total, + // including all existing sales ever. Given in product-specific + // units. + // A value of -1 indicates "infinite" (i.e. for "electronic" books). + total_stock: Integer; + + // Number of units of the product that have already been sold. + total_sold: Integer; + + // Number of units of the product that were lost (spoiled, stolen, etc.) + total_lost: Integer; + + // Identifies where the product is in stock. + address: Location; + + // Identifies when we expect the next restocking to happen. + next_restock?: Timestamp; + + } + + +Reserving inventory +------------------- + +.. http:post:: /private/products/$PRODUCT_ID/lock + + This is used to lock a certain quantity of the product for a limited + duration while the customer assembles a complete order. Note that + frontends do not have to "unlock", they may rely on the timeout as + given in the ``duration`` field. Re-posting a lock with a different + ``duration`` or ``quantity`` updates the existing lock for the same UUID + and does not result in a conflict. + + Unlocking by using a ``quantity`` of zero is is + optional but recommended if customers remove products from the + shopping cart. Note that actually POSTing to ``/orders`` with set + ``manage_inventory`` and using ``lock_uuid`` will **transition** the + lock to the newly created order (which may have a different ``duration`` + and ``quantity`` than what was requested in the lock operation). + If an order is for fewer items than originally locked, the difference + is automatically unlocked. + + **Request:** + + The request must be a `LockRequest`. + + **Response:** + + :status 204 No content: + The backend has successfully locked (or unlocked) the requested ``quantity``. :status 404 Not found: - The merchant backend could not find the order or the instance - and thus cannot process the request. - :status 409 Conflict: - The provided contract hash does not match this order. + The backend has does not know this product. + :status 410 Gone: + The backend does not have enough of product in stock. - .. ts:def:: PaidRequest + .. ts:def:: LockRequest - interface PaidRequest { - // Signature on ``TALER_PaymentResponsePS`` with the public - // key of the merchant instance. - sig: EddsaSignature; + interface LockRequest { - // hash of the order's contract terms (this is used to authenticate the - // wallet/customer and to enable signature verification without - // database access). - h_contract: HashCode; + // UUID that identifies the frontend performing the lock + lock_uuid: UUID; + + // How long does the frontend intend to hold the lock + duration: RelativeTime; + + // How many units should be locked? + quantity: Integer; - // Session id for which the payment is proven. - session_id: string; } -.. _order-abort: -.. http:post:: /orders/$ORDER_ID/abort +Removing products from inventory +-------------------------------- - Abort paying for an order and obtain a refund for coins that - were already deposited as part of a failed payment. +.. http:delete:: /private/products/$PRODUCT_ID + + Delete information about a product. Fails if the product is locked by + anyone. + + **Response:** + + :status 204 No content: + The backend has successfully deleted the product. + :status 404 Not found: + The backend does not know the instance or the product. + :status 409 Conflict: + The backend refuses to delete the product because it is locked. + + +------------------ +Payment processing +------------------ + +To process Taler payments, a merchant must first setup an order with +the merchant backend. The order is then claimed by a wallet, and +paid by the wallet. The merchant can check the payment status of the +order. Once the order is paid, the merchant may (for a limited time) +grant refunds on the order. + +Creating orders +--------------- + +.. _post-order: + +.. http:post:: /private/orders + + Create a new order that a customer can pay for. + + This request is **not** idempotent unless an ``order_id`` is explicitly specified. + However, while repeating without an ``order_id`` will create another order, that is + generally pretty harmless (as long as only one of the orders is returned to the wallet). + + .. note:: + + This endpoint does not return a URL to redirect your user to confirm the + payment. In order to get this URL use :http:get:/orders/$ORDER_ID. The + API is structured this way since the payment redirect URL is not unique + for every order, there might be varying parameters such as the session id. **Request:** - The request must be an `abort request `. We force the wallet - to specify the affected coins as it may only request for a subset of the coins - (i.e. because the wallet knows that some were double-spent causing the failure). - Also we need to know the coins because there may be two wallets "competing" over - the same order and one wants to abort while the other still proceeds with the - payment. Here we need to again know which subset of the deposits to abort. + The request must be a `PostOrderRequest`. **Response:** :status 200 OK: - The merchant accepted the request, and passed it on to the exchange. The body is a - a `merchant refund response `. Note that the exchange - MAY still have encountered errors in processing. Those will then be part of - the body. Wallets MUST carefully consider errors for each of the coins as - returned by the exchange. - :status 400 Bad request: - Either the client request is malformed or some specific processing error - happened that may be the fault of the client as detailed in the JSON body - of the response. - :status 403 Forbidden: - The ``h_contract`` does not match the $ORDER_ID. + The backend has successfully created the proposal. The response is a + :ts:type:`PostOrderResponse`. :status 404 Not found: - The merchant backend could not find the order or the instance - and thus cannot process the abort request. - :status 408 Request Timeout: - The merchant backend took too long getting a response from the exchange. - The wallet SHOULD retry soon. - :status 412 Precondition Failed: - Aborting the payment is not allowed, as the original payment did succeed. - It is possible that a different wallet succeeded with the payment. This - wallet should thus try to refresh all of the coins involved in the payment. - :status 424 Failed Dependency: - The merchant's interaction with the exchange failed in some way. - The error from the exchange is included. + The order given used products from the inventory, but those were not found + in the inventory. Or the merchant instance is unknown (including possibly the instance being not configured for new orders). Details in the + error code. NOTE: no good way to find out which product is not in the + inventory, we MAY want to specify that in the reply. + :status 409 Conflict: + A different proposal already exists under the specified order ID. + :status 410 Gone: + The order given used products from the inventory that are out of stock. + The response is a :ts:type:`OutOfStockResponse`. - The backend will return an `abort response `, which includes - verbatim the error codes received from the exchange's - :ref:`refund ` API. The frontend should pass the replies verbatim to - the browser/wallet. - .. ts:def:: AbortRequest + .. ts:def:: PostOrderRequest + + interface PostOrderRequest { + // The order must at least contain the minimal + // order detail, but can override all + order: Order; + + // if set, the backend will then set the refund deadline to the current + // time plus the specified delay. If it's not set, refunds will not be + // possible. + refund_delay?: RelativeTime; + + // specifies the payment target preferred by the client. Can be used + // to select among the various (active) wire methods supported by the instance. + payment_target?: string; + + // specifies that some products are to be included in the + // order from the inventory. For these inventory management + // is performed (so the products must be in stock) and + // details are completed from the product data of the backend. + inventory_products?: MinimalInventoryProduct[]; + + // Specifies a lock identifier that was used to + // lock a product in the inventory. Only useful if + // ``manage_inventory`` is set. Used in case a frontend + // reserved quantities of the individual products while + // the shopping card was being built. Multiple UUIDs can + // be used in case different UUIDs were used for different + // products (i.e. in case the user started with multiple + // shopping sessions that were combined during checkout). + lock_uuids?: UUID[]; + + // Should a token for claiming the order be generated? + // False can make sense if the ORDER_ID is sufficiently + // high entropy to prevent adversarial claims (like it is + // if the backend auto-generates one). Default is 'true'. + create_token?: boolean; + + } + + .. ts:def:: Order + + type Order : MinimalOrderDetail | ContractTerms; + + The following fields must be specified in the ``order`` field of the request. Other fields from + `ContractTerms` are optional, and will override the defaults in the merchant configuration. + + .. ts:def:: MinimalOrderDetail + + interface MinimalOrderDetail { + // Amount to be paid by the customer + amount: Amount; + + // Short summary of the order + summary: string; + + // URL that will show that the order was successful after + // it has been paid for. Optional. When POSTing to the + // merchant, the placeholder "${ORDER_ID}" will be + // replaced with the actual order ID (useful if the + // order ID is generated server-side and needs to be + // in the URL). + fulfillment_url?: string; + } + + The following fields can be specified if the order is inventory-based. + In this case, the backend can compute the amounts from the prices given + in the inventory. Note that if the frontend does give more details + (towards the ContractTerms), this will override those details + (including total price) that would otherwise computed based on information + from the inventory. - interface AbortRequest { + .. ts:def:: ProductSpecification - // hash of the order's contract terms (this is used to authenticate the - // wallet/customer in case $ORDER_ID is guessable). - h_contract: HashCode; + type ProductSpecification : (MinimalInventoryProduct | Product); - // List of coins the wallet would like to see refunds for. - // (Should be limited to the coins for which the original - // payment succeeded, as far as the wallet knows.) - coins: AbortingCoin[]; - } - .. ts:def:: AbortingCoin + .. ts:def:: MinimalInventoryProduct - interface AbortingCoin { - // Public key of a coin for which the wallet is requesting an abort-related refund. - coin_pub: EddsaPublicKey; + Note that if the frontend does give details beyond these, + it will override those details (including price or taxes) + that the backend would otherwise fill in via the inventory. - // The amount to be refunded (matches the original contribution) - contribution: Amount; + interface MinimalInventoryProduct { + // Which product is requested (here mandatory!) + product_id: string; - // URL of the exchange this coin was withdrawn from. - exchange_url: string; + // How many units of the product are requested + quantity: Integer; } - .. ts:def:: AbortResponse + .. ts:def:: PostOrderResponse - interface AbortResponse { + interface PostOrderResponse { + // Order ID of the response that was just created + order_id: string; - // List of refund responses about the coins that the wallet - // requested an abort for. In the same order as the 'coins' - // from the original request. - // The rtransaction_id is implied to be 0. - refunds: MerchantAbortPayRefundStatus[]; + // Token that authorizes the wallet to claim the order. + // Provided only if "create_token" was set to 'true' + // in the request. + token?: ClaimToken; } - .. ts:def:: MerchantAbortPayRefundStatus - type MerchantAbortPayRefundStatus = - | MerchantAbortPayRefundSuccessStatus - | MerchantAbortPayRefundFailureStatus; + .. ts:def:: OutOfStockResponse - .. ts:def:: MerchantAbortPayRefundFailureStatus + interface OutOfStockResponse { - // Details about why a refund failed. - interface MerchantAbortPayRefundFailureStatus { - // Used as tag for the sum type RefundStatus sum type. - type: "failure" + // Product ID of an out-of-stock item + product_id: string; - // HTTP status of the exchange request, must NOT be 200. - exchange_status: Integer; + // Requested quantity + requested_quantity: Integer; - // Taler error code from the exchange reply, if available. - exchange_code?: Integer; + // Available quantity (must be below ``requested_quanitity``) + available_quantity: Integer; - // If available, HTTP reply from the exchange. - exchange_reply?: Object; + // When do we expect the product to be again in stock? + // Optional, not given if unknown. + restock_expected?: Timestamp; } - .. ts:def:: MerchantAbortPayRefundSuccessStatus - // Additional details needed to verify the refund confirmation signature - // (``h_contract_terms`` and ``merchant_pub``) are already known - // to the wallet and thus not included. - interface MerchantAbortPayRefundSuccessStatus { - // Used as tag for the sum type MerchantCoinRefundStatus sum type. - type: "success" +Inspecting orders +----------------- - // HTTP status of the exchange request, 200 (integer) required for refund confirmations. - exchange_status: 200; +.. http:get:: /private/orders - // the EdDSA :ref:`signature` (binary-only) with purpose - // `TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND` using a current signing key of the - // exchange affirming the successful refund - exchange_sig: EddsaSignature; + Returns known orders up to some point in the past. - // public EdDSA key of the exchange that was used to generate the signature. - // Should match one of the exchange's signing keys from /keys. It is given - // explicitly as the client might otherwise be confused by clock skew as to - // which signing key was used. - exchange_pub: EddsaPublicKey; - } + **Request:** + :query paid: *Optional*. If set to yes, only return paid orders, if no only unpaid orders. Do not give (or use "all") to see all orders regardless of payment status. + :query refunded: *Optional*. If set to yes, only return refunded orders, if no only unrefunded orders. Do not give (or use "all") to see all orders regardless of refund status. + :query wired: *Optional*. If set to yes, only return wired orders, if no only orders with missing wire transfers. Do not give (or use "all") to see all orders regardless of wire transfer status. + :query date: *Optional.* Time threshold, see ``delta`` for its interpretation. Defaults to the oldest or most recent entry, depending on ``delta``. + :query start: *Optional*. Row number threshold, see ``delta`` for its interpretation. Defaults to ``UINT64_MAX``, namely the biggest row id possible in the database. + :query delta: *Optional*. takes value of the form ``N (-N)``, so that at most ``N`` values strictly older (younger) than ``start`` and ``date`` are returned. Defaults to ``-20`` to return the last 20 entries (before ``start`` and/or ``date``). + :query timeout_ms: *Optional*. Timeout in milli-seconds to wait for additional orders if the answer would otherwise be negative (long polling). Only useful if delta is positive. Note that the merchant MAY still return a response that contains fewer than delta orders. -.. http:patch:: /private/orders/$ORDER_ID/forget + **Response:** - Forget fields in an order's contract terms that the merchant no - longer needs. + :status 200 OK: + The response is an `OrderHistory`. - **Request:** + .. ts:def:: OrderHistory - The request must be a `forget request `. The fields specified - must have been marked as forgettable when the contract was created. Fields in - the request that are not in the `contract terms ` are ignored. + interface OrderHistory { + // timestamp-sorted array of all orders matching the query. + // The order of the sorting depends on the sign of ``delta``. + orders : OrderHistoryEntry[]; + } - A valid - JSON path is defined as a string beginning with ``$.`` that follows the dot - notation: ``$.wire_fee``, for example. The ``$`` represents the `contract terms ` - object, and an identifier following a ``.`` represents the field of that - identifier belonging to the object preceding the dot. Arrays can be indexed - by an non-negative integer within brackets: ``$.products[1]``. An asterisk ``*`` - can be used to index an array as a wildcard, which expands the path into a - list of paths containing one path for - each valid array index: ``$.products[*].description``. For a path to be valid, - it must end with a reference to a field of an object (it cannot end with an - array index or wildcard). - **Response:** + .. ts:def:: OrderHistoryEntry - :status 200 OK: - The merchant deleted the specified fields from the contract of - order $ORDER_ID. - :status 400 Bad request: - The request is malformed or one of the paths is invalid. - :status 404 Not found: - The merchant backend could not find the order or the instance - and thus cannot process the abort request. - :status 409 Conflict: - The request includes a field that was not marked as forgettable, so - the merchant cannot delete that field. + interface OrderHistoryEntry { - .. ts:def:: ForgetRequest + // order ID of the transaction related to this entry. + order_id: string; - interface ForgetRequest { + // row ID of the order in the database + row_id: number; - // Array of valid JSON paths to forgettable fields in the order's - // contract terms. - fields: string[]; - } + // when the order was created + timestamp: Timestamp; + // the amount of money the order is for + amount: Amount; + + // the summary of the order + summary: string; + + // whether some part of the order is refundable, + // that is the refund deadline has not yet expired + // and the total amount refunded so far is below + // the value of the original transaction. + refundable: boolean; + + // whether the order has been paid or not + paid: boolean; + } .. http:get:: /private/orders/$ORDER_ID @@ -1404,101 +1760,62 @@ Payment processing coin_pub: CoinPublicKey; } + +Private order data cleanup +-------------------------- -.. http:get:: /orders/$ORDER_ID - - Query the payment status of an order. This endpoint is for the wallet. - When the wallet goes to this URL and it is unpaid, - they will be prompted for payment. - This endpoint typically also supports requests with the "Accept" header - requesting "text/html". In this case, an HTML response suitable for - triggering the interaction with the wallet is returned, with ``timeout_ms`` - ignored (treated as zero). If the backend installation does not include the - required HTML templates, a 406 status code is returned. - - In the case that the request was made with a claim token (even the wrong one) - and the order was claimed and paid, the server will redirect the client to - the fulfillment URL. This redirection will happen with a 302 status code - if the "Accept" header specified "text/html", and with a 202 status code - otherwise. - - **Request:** - - :query h_contract=HASH: hash of the order's contract terms (this is used to authenticate the wallet/customer in case $ORDER_ID is guessable). Required once an order was claimed. - :query token=TOKEN: *Optional*. Authorizes the request via the claim token that was returned in the `PostOrderResponse`. Used with unclaimed orders only. Whether token authorization is required is determined by the merchant when the frontend creates the order. - :query session_id=STRING: *Optional*. Session ID that the payment must be bound to. If not specified, the payment is not session-bound. - :query timeout_ms=NUMBER: *Optional.* If specified, the merchant backend will - wait up to ``timeout_ms`` milliseconds for completion of the payment before - sending the HTTP response. A client must never rely on this behavior, as the - merchant backend may return a response immediately. - :query refund=AMOUNT: *Optional*. Indicates that we are polling for a refund above the given AMOUNT. Only useful in combination with timeout. - :query await_refund_obtained=BOOLEAN: *Optional*. If set to "yes", poll for the order's pending refunds to be picked up. - - **Response:** - - :status 200 OK: - The response is a `StatusPaidResponse`. - :status 202 Accepted: - The response is a `StatusGotoResponse`. Only returned if the content type requested was not HTML. - :status 302 Found: - The client should go to the indicated location. Only returned if the content type requested was HTML. - :status 402 PaymentRequired: - The response is a `StatusUnpaidResponse`. - :status 403 Forbidden: - The ``h_contract`` (or the ``token`` for unclaimed orders) does not match the order - and we have no fulfillment URL in the contract. - :status 410 Gone: - The response is a `StatusGoneResponse`. - :status 404 Not found: - The merchant backend is unaware of the order. - :status 406 Not Acceptable: - The merchant backend could not load the template required to generate a reply in the desired format. (Likely HTML templates were not properly installed.) - - .. ts:def:: StatusPaidResponse +Some orders may contain sensitive information that the merchant may not want +to retain after fulfillment, such as the customer's shipping address. By +initially labeling these order components as forgettable, the merchant can +later tell the backend to forget those details (without changing the hash of +the contract!) to minimize risks from information leakage. - interface StatusPaid { - // Was the payment refunded (even partially, via refund or abort)? - refunded: boolean; - // Is any amount of the refund still waiting to be picked up (even partially) - refund_pending: boolean; +.. http:patch:: /private/orders/$ORDER_ID/forget - // Amount that was refunded in total. - refund_amount: Amount; - } + Forget fields in an order's contract terms that the merchant no + longer needs. - .. ts:def:: StatusGotoResponse + **Request:** - interface StatusGotoResponse { - // The client should go to the fulfillment URL, it may be ready or - // might have some other interesting status. - fulfillment_url: string; - } + The request must be a `forget request `. The fields specified + must have been marked as forgettable when the contract was created. Fields in + the request that are not in the `contract terms ` are ignored. - .. ts:def:: StatusUnpaidResponse + A valid + JSON path is defined as a string beginning with ``$.`` that follows the dot + notation: ``$.wire_fee``, for example. The ``$`` represents the `contract terms ` + object, and an identifier following a ``.`` represents the field of that + identifier belonging to the object preceding the dot. Arrays can be indexed + by an non-negative integer within brackets: ``$.products[1]``. An asterisk ``*`` + can be used to index an array as a wildcard, which expands the path into a + list of paths containing one path for + each valid array index: ``$.products[*].description``. For a path to be valid, + it must end with a reference to a field of an object (it cannot end with an + array index or wildcard). - interface StatusUnpaidResponse { - // URI that the wallet must process to complete the payment. - taler_pay_uri: string; + **Response:** - // Status URL, can be used as a redirect target for the browser - // to show the order QR code / trigger the wallet. - fulfillment_url?: string; + :status 200 OK: + The merchant deleted the specified fields from the contract of + order $ORDER_ID. + :status 400 Bad request: + The request is malformed or one of the paths is invalid. + :status 404 Not found: + The merchant backend could not find the order or the instance + and thus cannot process the abort request. + :status 409 Conflict: + The request includes a field that was not marked as forgettable, so + the merchant cannot delete that field. - // Alternative order ID which was paid for already in the same session. - // Only given if the same product was purchased before in the same session. - already_paid_order_id?: string; - } + .. ts:def:: ForgetRequest - .. ts:def:: StatusGoneResponse + interface ForgetRequest { - // The client tried to access the order via the claim - // token (and not a valid h_contract), but the order can't be claimed - // anymore, as it is already paid. - interface StatusGoneResponse { - // Fulfillment URL for the order. - fulfillment_url: string; + // Array of valid JSON paths to forgettable fields in the order's + // contract terms. + fields: string[]; } @@ -1572,119 +1889,18 @@ Giving Refunds } -.. http:post:: /orders/$ORDER_ID/refund - - Obtain refunds for an order. After talking to the exchange, the refunds will - no longer be pending if processed successfully. - - **Request:** - - The request body is a `WalletRefundRequest` object. - - **Response:** - - :status 200 OK: - The response is a `WalletRefundResponse`. - :status 204 No content: - There are no refunds for the order. - :status 403 Forbidden: - The ``h_contract`` does not match the order. - :status 404 Not found: - The merchant backend is unaware of the order. - - .. ts:def:: WalletRefundRequest - - interface WalletRefundRequest { - // hash of the order's contract terms (this is used to authenticate the - // wallet/customer). - h_contract: HashCode; - } - - .. ts:def:: WalletRefundResponse - - interface WalletRefundResponse { - // Amount that was refunded in total. - refund_amount: Amount; - - // Successful refunds for this payment, empty array for none. - refunds: MerchantCoinRefundStatus[]; - - // Public key of the merchant. - merchant_pub: EddsaPublicKey; - - } - - .. ts:def:: MerchantCoinRefundStatus - - type MerchantCoinRefundStatus = - | MerchantCoinRefundSuccessStatus - | MerchantCoinRefundFailureStatus; - - .. ts:def:: MerchantCoinRefundFailureStatus - - // Details about why a refund failed. - interface MerchantCoinRefundFailureStatus { - // Used as tag for the sum type RefundStatus sum type. - type: "failure"; - - // HTTP status of the exchange request, must NOT be 200. - exchange_status: Integer; - - // Taler error code from the exchange reply, if available. - exchange_code?: Integer; - - // If available, HTTP reply from the exchange. - exchange_reply?: Object; - - // Refund transaction ID. - rtransaction_id: Integer; - - // public key of a coin that was refunded - coin_pub: EddsaPublicKey; - - // Amount that was refunded, including refund fee charged by the exchange - // to the customer. - refund_amount: Amount; - } - - .. ts:def:: MerchantCoinRefundSuccessStatus - - // Additional details needed to verify the refund confirmation signature - // (``h_contract_terms`` and ``merchant_pub``) are already known - // to the wallet and thus not included. - interface MerchantCoinRefundSuccessStatus { - // Used as tag for the sum type MerchantCoinRefundStatus sum type. - type: "success"; - - // HTTP status of the exchange request, 200 (integer) required for refund confirmations. - exchange_status: 200; - - // the EdDSA :ref:`signature` (binary-only) with purpose - // `TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND` using a current signing key of the - // exchange affirming the successful refund - exchange_sig: EddsaSignature; - - // public EdDSA key of the exchange that was used to generate the signature. - // Should match one of the exchange's signing keys from /keys. It is given - // explicitly as the client might otherwise be confused by clock skew as to - // which signing key was used. - exchange_pub: EddsaPublicKey; - - // Refund transaction ID. - rtransaction_id: Integer; - - // public key of a coin that was refunded - coin_pub: EddsaPublicKey; +----------------------- +Tracking Wire Transfers +----------------------- - // Amount that was refunded, including refund fee charged by the exchange - // to the customer. - refund_amount: Amount; - } +This API is used by merchants that want to track the payments from the +exchange to be sure that they have been paid on time. By telling the merchant +backend about all incoming wire transfers, the backend can detect if an +exchange failed to perform a wire transfer that was due. ------------------------- -Tracking Wire Transfers ------------------------- +Informing the backend about incoming wire transfers +--------------------------------------------------- .. http:post:: /private/transfers @@ -1822,7 +2038,7 @@ Tracking Wire Transfers // Master public key of the exchange master_pub: EddsaPublicKey; - } + } .. ts:def:: TrackTransferConflictDetails @@ -1906,6 +2122,8 @@ Tracking Wire Transfers } +Querying known wire transfers +----------------------------- .. http:get:: /private/transfers @@ -1980,12 +2198,32 @@ Tracking Wire Transfers } - - -------------------- -Giving Customer Tips +Backend: Giving tips -------------------- +Tips are a way for websites to give small amounts of e-cash to visitors (for +example as a financial reward for providing information or watching +advertisements). Tips are non-contractual: neither merchant nor consumer +have any contractual information about the other party as a result of the +tip. + + +Create reserve +-------------- + +Reserves are basically funds a merchant has provided +to an exchange for a tipping campaign. Each reserve +has a limited lifetime (say 2--4 weeks). Any funds +not used to tip customers will automatically be wired +back from the exchange to the originating account. + +To begin tipping, a merchant must tell the backend +to setup a reserve. The backend will return a +reserve public key which must be used as the wire +transfer subject when wiring the tipping campaign +funds to the exchange. + .. _tips: .. http:post:: /private/reserves @@ -2088,6 +2326,9 @@ Giving Customer Tips active: boolean; } + +Query funds remaining +--------------------- .. http:get:: /private/reserves/$RESERVE_PUB @@ -2157,6 +2398,9 @@ Giving Customer Tips } +Authorizing tips +---------------- + .. http:post:: /private/reserves/$RESERVE_PUB/authorize-tip Authorize creation of a tip from the given reserve. @@ -2228,6 +2472,9 @@ Giving Customer Tips in all of the reserves of the instance. +Deleting reserves +----------------- + .. http:delete:: /private/reserves/$RESERVE_PUB Delete information about a reserve. Fails if the reserve still has @@ -2250,6 +2497,8 @@ Giving Customer Tips The backend refuses to delete the reserve (committed tips awaiting pickup). +Checking tip status +------------------- .. http:get:: /private/tips/$TIP_ID @@ -2303,46 +2552,6 @@ Giving Customer Tips } - -.. http:get:: /tips/$TIP_ID - - Handle request from wallet to provide details about a tip. - - This endpoint typically also supports requests with the "Accept" header - requesting "text/html". In this case, an HTML response suitable for - triggering the interaction with the wallet is returned. If the backend - installation does not include the required HTML templates, a 406 status - code is returned. - - **Response:** - - :status 200 OK: - A tip is being returned. The backend responds with a `TipInformation`. - :status 404 Not Found: - The tip identifier is unknown. - :status 406 Not Acceptable: - The merchant backend could not load the template required to generate a reply in the desired format. (Likely HTML templates were not properly installed.) - :status 410 Gone: - A tip has been fully claimed. The JSON reply still contains the `TipInformation`. - - .. ts:def:: TipInformation - - interface TipInformation { - - // Exchange from which the tip will be withdrawn. Needed by the - // wallet to determine denominations, fees, etc. - exchange_url: string; - - // (remaining) amount of the tip (including fees). - tip_amount: Amount; - - // Timestamp indicating when the tip is set to expire (may be in the past). - // Note that tips that have expired MAY also result in a 404 response. - expiration: Timestamp; - } - - - .. http:get:: /private/tips Return the list of all tips. @@ -2385,68 +2594,17 @@ Giving Customer Tips -.. http:post:: /tips/$TIP_ID/pickup - - Handle request from wallet to pick up a tip. - - **Request:** - - The request body is a `TipPickupRequest` object. - - **Response:** - - :status 200 OK: - A tip is being returned. The backend responds with a `TipResponse` - :status 401 Unauthorized: - The tip amount requested exceeds the tip. - :status 404 Not Found: - The tip identifier is unknown. - :status 409 Conflict: - Some of the denomination key hashes of the request do not match those currently available from the exchange (hence there is a conflict between what the wallet requests and what the merchant believes the exchange can provide). - :status 410 Gone: - The tip has expired. - - .. ts:def:: TipPickupRequest - - interface TipPickupRequest { - - // List of planches the wallet wants to use for the tip - planchets: PlanchetDetail[]; - } - - .. ts:def:: PlanchetDetail - - interface PlanchetDetail { - // Hash of the denomination's public key (hashed to reduce - // bandwidth consumption) - denom_pub_hash: HashCode; - - // coin's blinded public key - coin_ev: CoinEnvelope; - } - - .. ts:def:: TipResponse - - interface TipResponse { - - // Blind RSA signatures over the planchets. - // The order of the signatures matches the planchets list. - blind_sigs: BlindSignature[]; - } - - .. ts:def:: BlindSignature - - interface BlindSignature { - - // The (blind) RSA signature. Still needs to be unblinded. - blind_sig: BlindedRsaSignature; - } - - ------------------ The Contract Terms ------------------ +This section describes the overall structure of +the contract terms that are the foundation for +Taler payments. + +FIXME: the "forgettable" attribute is not +properly specified here! + .. _contract-terms: The contract terms must have the following structure: -- cgit v1.2.3