taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

commit f2d823a3c33b481c1b8f813d6f3caa31e1ddc926
parent 546a40d319677d0553c882605840e2e245402238
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Mon, 20 Jul 2026 11:11:34 +0200

dd96 to merchant api changes

Diffstat:
Mcore/api-merchant.rst | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/merchant/delete-private-orders-ORDER_ID.rst | 16++++++++++++----
Mcore/merchant/get-private-orders-ORDER_ID.rst | 34++++++++++++++++++++++++++++++++++
Acore/merchant/post-private-orders-ORDER_ID-collect.rst | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/merchant/post-private-orders-ORDER_ID-refund-external.rst | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 268 insertions(+), 4 deletions(-)

diff --git a/core/api-merchant.rst b/core/api-merchant.rst @@ -67,6 +67,12 @@ The currently implemented protocol version is **v32**. challenge-signature OTP algorithms, token choices in fixed-order templates and same-key token re-issue for anonymous access control (see :doc:`DD 97 </design-documents/097-project-sesame>`) +* ``vMixedPayments``: adds the additive ``amount_external`` field for orders + settled partly outside of Taler, the ``/private/orders/$ORDER_ID/collect`` + endpoint for completing zero-Taler orders without a wallet, the + ``/private/orders/$ORDER_ID/refund-external`` endpoint for recording + externally settled refunds, and exposes both through order status + (see :doc:`DD 96 </design-documents/096-partial-payments>`) **Ideas for future version:** @@ -572,6 +578,18 @@ Inspecting orders .. include:: merchant/get-private-orders-ORDER_ID.rst +.. _collect-order: + +Collecting zero-Taler orders +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Orders whose Taler ``amount`` is zero, for example because the full total +was settled through ``amount_external``, can be completed by the merchant +backend itself instead of a customer wallet. + +.. include:: merchant/post-private-orders-ORDER_ID-collect.rst + + .. _private-order-data-cleanup: Private order data cleanup @@ -596,6 +614,8 @@ Approving Refunds .. include:: merchant/post-private-orders-ORDER_ID-refund.rst +.. include:: merchant/post-private-orders-ORDER_ID-refund-external.rst + ----------------------- Tracking Wire Transfers @@ -1473,6 +1493,41 @@ Orders // A value of "never" is not allowed. wire_transfer_deadline?: Timestamp; + // Payments for this order that were settled outside of Taler, + // for example in cash or by card. The ``amount`` field above (or + // the selected choice's ``amount`` for v1 orders) remains the + // amount paid with Taler; the full order total is the sum of that + // amount and all entries here. If present, all entries and (for + // v1 orders) all choices must use the currency of the Taler + // ``amount``. Never contains a ``taler`` entry. + // @since protocol **vMixedPayments**. + amount_external?: ExternalPaymentInfo[]; + + } + + .. ts:def:: ExternalPaymentInfo + + interface ExternalPaymentInfo { + // External payment method, for example "cash" or "card". + // Must be a stable, non-empty ASCII identifier and must + // never be "taler". + method: string; + + // Identifier of the payment action within the order, unique + // among the entries of this ``amount_external`` array. + // Examples: "cash1", "card1", "card2". + id: string; + + // Amount covered by this payment action. Must use the same + // currency as the Taler ``amount`` of the order. + amount: Amount; + + // Additional method-specific fields for reconciliation or audit, + // for example a cashier number, terminal identifier or + // authorization code. These must be flat scalar values (no + // nested objects or arrays) so that the merchant portal can + // render ``amount_external`` as a generic table. + [field: string]: string | Amount | Integer | boolean | null; } .. _product-entry: @@ -1733,6 +1788,14 @@ and is thus not yet buyer-specific. // Exchanges that the merchant accepts even if it does not accept any auditors that audit them. exchanges: Exchange[]; + // Payments for this order that were settled outside of Taler, + // for example in cash or by card. The ``amount`` field (or the + // selected choice's ``amount`` for v1 contracts) remains the amount + // paid with Taler; the full order total is the sum of that amount + // and all entries here. See `ExternalPaymentInfo`. + // @since protocol **vMixedPayments**. + amount_external?: ExternalPaymentInfo[]; + } .. ts:def:: ContractChoice diff --git a/core/merchant/delete-private-orders-ORDER_ID.rst b/core/merchant/delete-private-orders-ORDER_ID.rst @@ -4,11 +4,16 @@ last 10 years (or whatever ``TAX_RECORD_EXPIRATION`` is set to) or was claimed but is unpaid and thus still a valid offer. + Since protocol **vMixedPayments**, an order that carries externally + settled payments (``amount_external``) can only be deleted by explicitly + passing ``force``, as such an order may have received payments outside of + Taler that the merchant should resolve manually first. + **Request:** :query force: *Optional*. If set to YES, the order will be deleted - even if it was already claimed. Note that even with force, a - paid order cannot be deleted. + even if it was already claimed or carries externally settled + payments. Note that even with force, a paid order cannot be deleted. **Response:** @@ -21,8 +26,11 @@ Returned with ``TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN``. :http:statuscode:`409 Conflict`: The backend refuses to delete the order. - Returned with ``TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID`` or - ``TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT``. + Returned with ``TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID``, + ``TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT`` or + ``TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_EXTERNALLY_PAID`` (the latter + since protocol **vMixedPayments**, when the order carries externally + settled payments and ``force`` was not set). :http:statuscode:`500 Internal Server Error`: The server experienced an internal failure. Returned with ``TALER_EC_GENERIC_DB_COMMIT_FAILED`` or diff --git a/core/merchant/get-private-orders-ORDER_ID.rst b/core/merchant/get-private-orders-ORDER_ID.rst @@ -119,6 +119,14 @@ // refunded coin; empty array if there are no refunds. refund_details: RefundDetails[]; + // Refunds settled outside of Taler that were recorded for this + // order via + // [/instances/$INSTANCE]/private/orders/$ORDER_ID/refund-external. + // These are bookkeeping entries only; empty array if there are + // none. + // @since protocol **vMixedPayments**. + refunds_external: ExternalRefundInfo[]; + // Status URL, can be used as a redirect target for the browser // to show the order QR code / trigger the wallet. order_status_url: string; @@ -205,6 +213,32 @@ amount: Amount; } + .. ts:def:: ExternalRefundInfo + + interface ExternalRefundInfo { + // Method by which the funds were returned to the customer, + // for example "cash" or "card". May differ from the methods used + // to pay the order. Never "taler". + method: string; + + // The ``id`` of the ``amount_external`` entry this refund + // reverses, when the refund maps to a specific original payment. + payment_id?: string; + + // Amount returned to the customer via the external method. + amount: Amount; + + // Human-readable refund justification. + reason: string; + + // When the external refund was recorded at the backend. + timestamp: Timestamp; + + // Additional flat method-specific fields, same rules as in + // `ExternalPaymentInfo`. + [field: string]: string | Amount | Integer | boolean | null; + } + .. ts:def:: TransactionWireTransfer interface TransactionWireTransfer { diff --git a/core/merchant/post-private-orders-ORDER_ID-collect.rst b/core/merchant/post-private-orders-ORDER_ID-collect.rst @@ -0,0 +1,70 @@ +.. http:post:: [/instances/$INSTANCE]/private/orders/$ORDER_ID/collect + + Complete a zero-Taler order without a customer wallet. This is used for + orders whose Taler ``amount`` is zero, for example because the full total + was settled through ``amount_external``. The merchant backend acts like a + customer wallet collecting a free order: it claims the order with a + backend-generated nonce and then marks the contract as paid. Payment + notifications, webhooks, triggers and inventory updates behave exactly as + for a wallet payment of a free order. + + The operation is idempotent: the backend derives the claim nonce + deterministically from the instance key and the order identifier, so + repeating the request after a success (or a crash between the claim and the + payment step) completes or replays harmlessly. + + Collection is only possible while the order is unclaimed. If a customer + wallet has already claimed the order, the wallet owns the order and must + execute the free payment itself. A point-of-sale application should + therefore offer the order QR code first and only collect after the customer + declined or a timeout passed. + + Collection is restricted to genuinely free Taler payments: the Taler + ``amount`` must be zero and, for v1 contracts, the selected choice must have + no ``inputs`` and no ``outputs``. + + **Required permission:** ``orders-write`` + + Since protocol **vMixedPayments**. + + **Request:** + + The request body is a `CollectRequest` object. + + **Response:** + + :http:statuscode:`204 No content`: + The order was successfully collected (or had already been collected). + :http:statuscode:`401 Unauthorized`: + The request is unauthorized. + :http:statuscode:`404 Not found`: + The order is unknown to the merchant. + Returned with ``TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN``. + :http:statuscode:`409 Conflict`: + The order cannot be collected by the backend. + Returned with + ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_COLLECT_ALREADY_CLAIMED`` + if the order was already claimed by a customer wallet, or with + ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_COLLECT_NOT_FREE`` + if the Taler amount is not zero or the selected choice redeems or + issues tokens. + :http:statuscode:`500 Internal Server Error`: + The server experienced an internal failure. + Returned with ``TALER_EC_GENERIC_DB_COMMIT_FAILED`` or + ``TALER_EC_GENERIC_DB_SOFT_FAILURE``. + + **Details:** + + .. ts:def:: CollectRequest + + interface CollectRequest { + // Session ID to store with the payment, like the ``session_id`` of + // a wallet payment. Allows a point-of-sale device to tag and later + // list the orders it collected. Absent for no session. + session_id?: string; + + // For v1 orders with multiple choices, the index of the choice to + // complete within the ``choices`` array of the contract terms. + // Defaults to the first choice. + choice_index?: Integer; + } diff --git a/core/merchant/post-private-orders-ORDER_ID-refund-external.rst b/core/merchant/post-private-orders-ORDER_ID-refund-external.rst @@ -0,0 +1,89 @@ +.. http:post:: [/instances/$INSTANCE]/private/orders/$ORDER_ID/refund-external + + Record a refund that was settled outside of Taler, for example a cash or + card return. Unlike Taler refunds, this is a bookkeeping entry only: the + point-of-sale or external payment integration performs the actual return of + funds, and no wallet pickup step exists or is needed. The recorded entries + are exposed through the order status as ``refunds_external``. + + External refunds are available for any *paid* order, including pure Taler + orders without ``amount_external``. This is relevant because Taler refunds + are constrained by the contract's refund deadline; once it has passed, the + amount can only be returned through an external method, which is recorded + here. + + The refund channel does not need to match the payment channel. The backend + validates external refunds against the order total: the cumulative + externally refunded amount must not exceed the full order total (the Taler + ``amount`` plus all ``amount_external`` entries) minus the amount already + refunded through Taler, and must use the currency of the order. + + **Required permission:** ``orders-refund`` + + Since protocol **vMixedPayments**. + + **Request:** + + The request body is an `ExternalRefundRequest` object. + + **Response:** + + :http:statuscode:`204 No content`: + The external refund was recorded. + :http:statuscode:`400 Bad Request`: + The request body is malformed, for example the ``method`` is empty, + is ``taler``, contains non-ASCII characters, or a method-specific + field is not a flat scalar value. + Returned with ``TALER_EC_GENERIC_PARAMETER_MALFORMED``. + :http:statuscode:`401 Unauthorized`: + The request is unauthorized. + :http:statuscode:`404 Not found`: + The order is unknown to the merchant. + Returned with ``TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN``. + :http:statuscode:`409 Conflict`: + The external refund could not be recorded. + Returned with + ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID`` + if the order is not paid, + ``TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH`` + if the refund uses a different currency than the order, or + ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_EXTERNAL_INCONSISTENT_AMOUNT`` + if the cumulative external refunds would exceed the order total minus + the amount already refunded through Taler. + :http:statuscode:`500 Internal Server Error`: + The server experienced an internal failure. + Returned with ``TALER_EC_GENERIC_DB_START_FAILED``, + ``TALER_EC_GENERIC_DB_FETCH_FAILED``, + ``TALER_EC_GENERIC_DB_STORE_FAILED``, + ``TALER_EC_GENERIC_DB_COMMIT_FAILED``, + ``TALER_EC_GENERIC_DB_SOFT_FAILURE``, + ``TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH`` or + ``TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID``. + + **Details:** + + .. ts:def:: ExternalRefundRequest + + interface ExternalRefundRequest { + // Method by which the funds were returned to the customer, + // for example "cash" or "card". May differ from the methods used + // to pay the order. Must be a stable, non-empty ASCII identifier + // and must never be "taler". + method: string; + + // Optionally, the ``id`` of the ``amount_external`` entry this + // refund reverses, when the refund maps to a specific original + // payment (for example a card transaction reversal). + payment_id?: string; + + // Amount returned to the customer via the external method. + // Must use the currency of the order. + amount: Amount; + + // Human-readable refund justification, mirroring Taler refunds. + reason: string; + + // Additional flat method-specific fields, same rules as in + // `ExternalPaymentInfo`. No nested objects or arrays. + [field: string]: string | Amount | Integer | boolean | null; + }