Contents

POST [/instances/$INSTANCE]/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. To get this URL use either GET [/instances/$INSTANCE]/orders/$ORDER_ID (with taler_pay_uri in the StatusUnpaidResponse), or GET [/instances/$INSTANCE]/private/orders/$ORDER_ID with the taler_pay_uri in the CheckPaymentUnpaidResponse). That said, it is also possible to construct the URL by combining the base URL with the information from the PostOrderResponse. 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.

Required permission: orders-write

Request:

The request must be a PostOrderRequest.

Response:

200 OK:

The backend has successfully created the proposal. The response is a PostOrderResponse.

404 Not found:

Possible reasons are:

  1. The order given used products from the inventory, but those were not found in the inventory.

  2. The merchant instance is unknown (including possibly the instance being not configured for new orders).

  3. The wire method specified is not supported by the backend.

  4. An OTP device ID was specified and is unknown.

Details in the error code. NOTE: currently the client has no good way to find out which product is not in the inventory, we MAY want to specify that in the reply.

409 Conflict:

A different proposal already exists under the specified order ID, or the requested currency is not supported by this backend. Details in the error code.

410 Gone:

The order given used products from the inventory that are out of stock. The response is a OutOfStockResponse.

451 Unavailable for Legal Reasons:

The order could not be created because of legal reasons, specifically no exchange would accept a payment at this time because we have not yet satisfied the respective legal requirements. The KYC status API can be used to determine details about how to proceed with the KYC process. Since v25, the body is an OrderRefusedErrorDetailResponse with an error code of MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS.

Details:

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
  // payment deadline plus the specified delay.
  // If it's not set, the default value of the backend might be
  // used. Note that both this value and the backend default
  // will be ignored if refund_deadline is set in order
  // as the refund_deadline takes precedence.
  // A value of "forever" is not allowed.
  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;

  // The session for which the payment is made (or replayed).
  // Only set for session-based payments.
  // Since protocol **v6**.
  session_id?: 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
  // inventory_products is set.  Used in case a frontend
  // reserved quantities of the individual products while
  // the shopping cart 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?: 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'.
  // Note: This is NOT related to tokens used for subscriptins or discounts.
  create_token?: boolean;

  // OTP device ID to associate with the order.
  // This parameter is optional.
  otp_id?: string;

}

The Order object represents the starting point for new ContractTerms. After validating and sanatizing all inputs, the merchant backend will add additional information to the order and create a new ContractTerms object that will be stored in the database.

type Order = (OrderV1 | OrderV0) & OrderCommon;
interface OrderV1 {
  // Version 1 order support discounts and subscriptions.
  // https://docs.taler.net/design-documents/046-mumimo-contracts.html
  // @since protocol **v21**
  version: 1;

  // List of contract choices that the customer can select from.
  // @since protocol **v21**
  choices?: OrderChoice[];
}
interface OrderV0 {
  // Optional, defaults to 0 if not set.
  version?: 0;

  // Total price for the transaction, including tip. The exchange will
  // subtract deposit fees from that amount before transferring it to
  // the merchant.
  amount: Amount;

  // Optional tip amount. Must match the currency of amount.
  // Since protocol **v25**.
  tip?: Amount;

  // Maximum total deposit fee accepted by the merchant for this contract.
  // Overrides defaults of the merchant instance.
  max_fee?: Amount;
}
interface OrderCommon {
  // Human-readable description of the whole purchase.
  summary: string;

  // Map from IETF BCP 47 language tags to localized summaries.
  summary_i18n?: { [lang_tag: string]: string };

  // Unique identifier for the order. Only characters
  // allowed are "A-Za-z0-9" and ".:_-".
  // Must be unique within a merchant instance.
  // For merchants that do not store proposals in their DB
  // before the customer paid for them, the order_id can be used
  // by the frontend to restore a proposal from the information
  // encoded in it (such as a short product identifier and timestamp).
  order_id?: string;

  // URL where the same contract could be ordered again (if
  // available). Returned also at the public order endpoint
  // for people other than the actual buyer (hence public,
  // in case order IDs are guessable).
  public_reorder_url?: string;

  // See documentation of fulfillment_url field in ContractTerms.
  // Either fulfillment_url or fulfillment_message must be specified.
  // When creating an order, the fulfillment URL can
  // contain ${ORDER_ID} which will be substituted with the
  // order ID of the newly created order.
  fulfillment_url?: string;

  // See documentation of fulfillment_message in ContractTerms.
  // Either fulfillment_url or fulfillment_message must be specified.
  fulfillment_message?: string;

  // Map from IETF BCP 47 language tags to localized fulfillment
  // messages.
  fulfillment_message_i18n?: { [lang_tag: string]: string };

  // Minimum age the buyer must have to buy.
  minimum_age?: Integer;

  // List of products that are part of the purchase.
  products?: ProductSold[];

  // Time when this contract was generated. If null, defaults to current
  // time of merchant backend.
  timestamp?: Timestamp;

  // After this deadline has passed, no refunds will be accepted.
  // Overrides deadline calculated from refund_delay in
  // PostOrderRequest.
  // A value of "never" is not allowed.
  refund_deadline?: Timestamp;

  // After this deadline, the merchant won't accept payments for the contract.
  // Overrides deadline calculated from default pay delay configured in
  // merchant backend.
  // A value of "never" is not allowed.
  pay_deadline?: Timestamp;

  // Transfer deadline for the exchange. Must be in the deposit permissions
  // of coins used to pay for this order.
  // Overrides deadline calculated from default wire transfer delay
  // configured in merchant backend. Must be after refund deadline.
  // A value of "never" is not allowed.
  wire_transfer_deadline?: Timestamp;

  // Base URL of the (public!) merchant backend API.
  // Must be an absolute URL that ends with a slash.
  // Defaults to the base URL this request was made to.
  merchant_base_url?: string;

  // Delivery location for (all!) products.
  delivery_location?: Location;

  // Time indicating when the order should be delivered.
  // May be overwritten by individual products.
  // Must be in the future.
  delivery_date?: Timestamp;

  // See documentation of auto_refund in ContractTerms.
  // Specifies for how long the wallet should try to get an
  // automatic refund for the purchase.
  auto_refund?: RelativeTime;

  // Extra data that is only interpreted by the merchant frontend.
  // Useful when the merchant needs to store extra information on a
  // contract without storing it separately in their database.
  // Must really be an Object (not a string, integer, float or array).
  extra?: Object;

  // Money pot to increment for whatever order payment amount
  // is not yet assigned to a pot via the ProductSold.
  // Not useful to wallets, only for
  // merchant-internal accounting.
  // Since protocol **v25**.
  order_default_money_pot?: Integer;

}

The OrderChoice object describes a possible choice within an order. The choice is done by the wallet and consists of in- and outputs. In the example of buying an article, the merchant could present the customer with the choice to use a valid subscription token or pay using a gift voucher. Available since protocol v21.

interface OrderChoice {
  // Total price for the choice. The exchange will subtract deposit
  // fees from that amount before transferring it to the merchant.
  amount: Amount;

  // Optional tip amount. Must match the currency of amount.
  // Since protocol **v25**.
  tip?: Amount;

  // Human readable description of the semantics of the choice
  // within the contract to be shown to the user at payment.
  description?: string;

  // Map from IETF 47 language tags to localized descriptions.
  description_i18n?: { [lang_tag: string]: string };

  // Inputs that must be provided by the customer, if this choice is selected.
  // Defaults to empty array if not specified.
  inputs?: OrderInput[];

  // Outputs provided by the merchant, if this choice is selected.
  // Defaults to empty array if not specified.
  outputs?: OrderOutput[];

  // Maximum total deposit fee accepted by the merchant for this contract.
  // Overrides defaults of the merchant instance.
  max_fee?: Amount;
}
// For now, only token inputs are supported.
type OrderInput = OrderInputToken;
interface OrderInputToken {

  // Token input.
  type: "token";

  // Token family slug as configured in the merchant backend. Slug is unique
  // across all configured tokens of a merchant.
  token_family_slug: string;

  // How many units of the input are required.
  // Defaults to 1 if not specified. Output with count == 0 are ignored by
  // the merchant backend.
  count?: Integer;

}
interface OrderOutputToken {

  // Token output.
  type: "token";

  // Token family slug as configured in the merchant backend. Slug is unique
  // across all configured tokens of a merchant.
  token_family_slug: string;

  // How many units of the output are issued by the merchant.
  // Defaults to 1 if not specified. Output with count == 0 are ignored by
  // the merchant backend.
  count?: Integer;

  // When should the output token be valid. Can be specified if the
  // desired validity period should be in the future (like selling
  // a subscription for the next month). Optional. If not given,
  // the validity is supposed to be "now" (time of order creation).
  valid_at?: Timestamp;

}
interface OrderOutputTaxReceipt {

  // Tax receipt output.
  type: "tax-receipt";

}

The following MinimalInventoryProduct can be provided if the parts of the order are inventory-based, that is if the PostOrderRequest uses inventory_products. For such products, which must be in the backend’s inventory, the backend can automatically fill in the amount and other details about the product that are known to it from its products table. Note that the inventory_products will be appended to the list of products that the frontend already put into the order. So if the frontend can sell additional non-inventory products together with inventory_products. Note that the backend will NOT update the amount of the order, so the frontend must already have calculated the total price — including the inventory_products.

// 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.
interface MinimalInventoryProduct {

  // Which product is requested (here mandatory!).
  product_id: string;

  // Legacy integer quantity.
  // Deprecated since **v25**;
  // defaults to 1 if both quantity and unit_quantity are absent.
  quantity?: Integer;

  // Preferred quantity string using "<integer>[.<fraction>]" syntax.
  // @since **v25**;
  unit_quantity?: string

  // Money pot to use for this product, overrides value from
  // the inventory if given.
  // Since **v25**.
  product_money_pot?: Integer;

}

Supply either quantity or unit_quantity when referencing inventory products. If both are missing the backend assumes a quantity of one. unit_quantity follows the same decimal-string rules as unit_total_stock.

interface PostOrderResponse {
  // Order ID of the response that was just created.
  order_id: string;

  // Deadline when the offer expires; the customer must pay before.
  // @since protocol **v21**.
  pay_deadline: Timestamp;

  // Token that authorizes the wallet to claim the order.
  // Provided only if "create_token" was set to 'true'
  // in the request.
  token?: ClaimToken;
}
interface OutOfStockResponse {

  // Product ID of an out-of-stock item.
  product_id: string;

  // Legacy integer quantity requested. Deprecated; see unit_requested_quantity.
  requested_quantity: Integer;

  // Requested quantity using "<integer>[.<fraction>]" syntax with up to six fractional digits.
  unit_requested_quantity: string;

  // Legacy integer availability (must be below requested_quantity).
  available_quantity: Integer;

  // Available quantity using "<integer>[.<fraction>]" syntax with up to six fractional digits.
  unit_available_quantity: string;

  // When do we expect the product to be again in stock?
  // Optional, not given if unknown.
  restock_expected?: Timestamp;
}
interface OrderRefusedErrorDetailResponse {

  // Numeric error code unique to the condition.
  // Will be MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS).
  code: ErrorCode;

  // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ...
  // Should give a human-readable hint about the error's nature. Optional, may change without notice!
  hint?: string;

  // Detail about why a specific exchange was rejected.
  // Note that an exchange that was allowed is not listed.
  // It is possible that no exchanges were rejected (in which
  // case this array would be empty) and still the operation
  // failed because the total of the allowed amounts per
  // exchange ended up below the order total. Thus, that
  // is ultimately always the cause here (as per the code),
  // but the *other* reasons why exchanges might have been
  // rejected could be enlightening to the user and are
  // thus provided here.
  exchange_rejections: ExchangeRejectionDetail;
}
interface ExchangeRejectionDetail {

  // Base URL of the rejected exchange
  exchange_url: string;

  // Numeric error code unique to why
  // this exchange was not acceptable.
  // Can be MERCHANT_GENERIC_CURRENCY_MISMATCH,
  // MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LEGALLY_REFUSED
  // (zero deposit limit, likely KYC required),
  // MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE
  // (we failed to download /keys from the exchange),
  // MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED
  // (none of our bank accounts has a compatible wire method)
  code: ErrorCode;

  // Human-readable description of the error.
  // Should give a human-readable hint about the error's nature.
  // Optional, may change without notice!
  hint?: string;

}