{
  "openapi": "3.1.0",
  "info": {
    "title": "SUMWARE Voucher Service — Public API",
    "version": "0.8.0",
    "summary": "Voucher / gift-card redemption for POS, eCom, and CRM integrations.",
    "description": "The public plane of the SUMWARE voucher service. POS terminals,\neCom storefronts, and CRM/loyalty connectors exchange an API key\nfor a short-lived JWT and call the endpoints under\n`/v1/redemption/*`.\n\nThe core contract is intentionally small:\n**check · load · redeem · refund**, plus two customer-bound\nsiblings — **customer-vouchers** (loyalty-card lookup) and\n**refund-to-credit** (alternative refund settlement).\n\nThere are two redemption paths the integrator needs to understand:\n\n- **Plain voucher code** — `code` field — used when the cashier\n  or customer scans an actual voucher; the code is a secret on the\n  order of a credit-card number.\n- **Ephemeral voucher handle** — `voucher_handle` field — issued\n  by the server when a customer is identified by their loyalty\n  card; opaque, store-bound, ~20-minute TTL. The handle never\n  reveals the underlying code, so the POS can drive\n  check/load/redeem on it without ever seeing the secret.\n\n`check`, `load`, and `redeem` accept either `code` or\n`voucher_handle` (exactly one). See\n`/docs/redemption/code-vs-handle` on the developer portal for the\nfull comparison.\n\nPhase 7 adds outbound **webhooks** — see the `webhooks` section\nbelow for the delivery contract subscribers must implement.\n",
    "contact": {
      "name": "SUMWARE Voucher",
      "url": "https://sumware.at/docs/",
      "email": "office@retail-butlers.at"
    }
  },
  "externalDocs": {
    "description": "SUMWARE developer portal",
    "url": "https://sumware.at/docs/"
  },
  "servers": [
    {
      "url": "https://api.sumware.at",
      "description": "Production"
    },
    {
      "url": "https://sandbox.api.sumware.at",
      "description": "Sandbox — throwaway vouchers, dedicated API keys"
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Exchange an API key for a short-lived redemption JWT."
    },
    {
      "name": "Redemption",
      "description": "Inspect, sell, redeem, and refund vouchers."
    },
    {
      "name": "Customer-bound",
      "description": "Loyalty-card lookup and alternative refund settlement."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/auth/token": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Exchange an API key for a redemption JWT",
        "description": "POS / eCom / CRM clients hold a long-lived API key (provisioned in the\nadmin cockpit, shown to the admin once). They exchange it at this\nendpoint for a short-lived JWT and use that JWT on every subsequent\ncall. The response's `channel` and (for POS clients) `store_id`\ndetermine which endpoints and which voucher scopes the JWT can reach.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              },
              "examples": {
                "default": {
                  "value": {
                    "api_key": "f2e1...64hex..."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                },
                "examples": {
                  "pos": {
                    "value": {
                      "access_token": "eyJhbGciOi...",
                      "token_type": "Bearer",
                      "expires_in": 900,
                      "channel": "pos",
                      "store_id": "ST-001",
                      "country": "AT"
                    }
                  },
                  "ecom": {
                    "value": {
                      "access_token": "eyJhbGciOi...",
                      "token_type": "Bearer",
                      "expires_in": 900,
                      "channel": "ecom",
                      "country": "AT"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/redemption/check": {
      "post": {
        "tags": [
          "Redemption"
        ],
        "summary": "Inspect a voucher; optionally place a store-scoped check-lock",
        "description": "Returns the voucher's current state and balance. Accepts either\n`code` (plain voucher) or `voucher_handle` (from a prior\ncustomer-vouchers lookup). POS clients may additionally pass\n`lock_minutes` to **temporarily lock the voucher to this store** —\nother stores and eCom are then refused redemption on it until the\nlock expires; the locking store itself is unaffected. eCom always\nrequires the PIN on the code path; on the handle path the\nstore-binding + TTL stand in for it.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckRequest"
              },
              "examples": {
                "code": {
                  "summary": "Plain voucher code (POS scan)",
                  "value": {
                    "code": "A5F3BBFF967CD2F3"
                  }
                },
                "pos-lock": {
                  "summary": "Plain code with a 5-minute lock",
                  "value": {
                    "code": "A5F3BBFF967CD2F3",
                    "lock_minutes": 5
                  }
                },
                "ecom": {
                  "summary": "eCom with PIN",
                  "value": {
                    "code": "A5F3BBFF967CD2F3",
                    "pin": "48213"
                  }
                },
                "handle": {
                  "summary": "Customer-bound handle (POS after loyalty-card scan)",
                  "value": {
                    "voucher_handle": "vh_8f3c…64hex…"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Voucher info.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckResponse"
                },
                "examples": {
                  "value-voucher": {
                    "value": {
                      "voucher_ref": "9d2f4a6f12c34c0eaba33d8c7b27cd11",
                      "kind": "payment",
                      "state": "sold",
                      "balance": {
                        "amount": 5000,
                        "currency": "EUR"
                      },
                      "valid_from": null,
                      "valid_to": null
                    }
                  },
                  "value-locked": {
                    "value": {
                      "voucher_ref": "9d2f4a6f12c34c0eaba33d8c7b27cd11",
                      "kind": "payment",
                      "state": "sold",
                      "balance": {
                        "amount": 5000,
                        "currency": "EUR"
                      },
                      "locked_until": "2026-05-23T14:32:11Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PinFault"
          },
          "404": {
            "description": "No voucher matches the supplied `code` or `voucher_handle`.\nAll failure modes on the handle path (unknown, expired,\nwrong store) collapse to `voucher_handle_invalid` — the\nendpoint cannot enumerate live handles.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "examples": {
                  "code-not-found": {
                    "value": {
                      "error": {
                        "code": "voucher_not_found",
                        "message": "no voucher matches this code"
                      }
                    }
                  },
                  "handle-invalid": {
                    "value": {
                      "error": {
                        "code": "voucher_handle_invalid",
                        "message": "no voucher matches this handle"
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/StateConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/redemption/load": {
      "post": {
        "tags": [
          "Redemption"
        ],
        "summary": "Sell or recharge value on a voucher",
        "description": "A first load on a freshly generated voucher creates its balance —\nthe **initial sale**. A later load on an already-sold voucher is a\n**recharge** and is permitted only by the voucher's recharge policy\n(`never` / `when_empty` / `below_threshold` / `anytime`). The\nledger entry's `origin` records which it was.\n\nFor a fixed-denomination voucher you may either omit `amount` (the\ndenomination is used) or pass an amount equal to it; otherwise the\nrequest is rejected. Flexible-value vouchers and recharges take\nthe requested `amount`.\n\nAccepts either `code` or `voucher_handle` (exactly one).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoadRequest"
              },
              "examples": {
                "fixed-sale": {
                  "summary": "Sell a €50 fixed voucher",
                  "value": {
                    "code": "A5F3BBFF967CD2F3",
                    "pos_id": "POS-03"
                  }
                },
                "flexible-sale": {
                  "summary": "Sell a flexible voucher for €30",
                  "value": {
                    "code": "A5F3BBFF967CD2F3",
                    "amount": {
                      "amount": 3000,
                      "currency": "EUR"
                    },
                    "pos_id": "POS-03"
                  }
                },
                "recharge": {
                  "summary": "Recharge €20 onto an already-sold voucher",
                  "value": {
                    "code": "A5F3BBFF967CD2F3",
                    "amount": {
                      "amount": 2000,
                      "currency": "EUR"
                    },
                    "pos_id": "POS-03"
                  }
                },
                "handle-sale": {
                  "summary": "Initial sale via customer-bound handle",
                  "value": {
                    "voucher_handle": "vh_8f3c…",
                    "pos_id": "POS-03"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Loaded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoadResponse"
                },
                "examples": {
                  "default": {
                    "value": {
                      "txn_id": "txn_42f9",
                      "voucher_ref": "9d2f4a6f12c34c0eaba33d8c7b27cd11",
                      "kind": "payment",
                      "loaded": {
                        "amount": 5000,
                        "currency": "EUR"
                      },
                      "balance_after": {
                        "amount": 5000,
                        "currency": "EUR"
                      },
                      "voucher_state": "sold",
                      "masked_code": "XXXXXXXXXXXXD2F3"
                    }
                  },
                  "handle": {
                    "summary": "Handle path — masked_code is empty (no plaintext to mask)",
                    "value": {
                      "txn_id": "txn_42fa",
                      "voucher_ref": "9d2f4a6f12c34c0eaba33d8c7b27cd11",
                      "kind": "payment",
                      "loaded": {
                        "amount": 5000,
                        "currency": "EUR"
                      },
                      "balance_after": {
                        "amount": 5000,
                        "currency": "EUR"
                      },
                      "voucher_state": "sold"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/VoucherNotFound"
          },
          "409": {
            "$ref": "#/components/responses/StateConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/redemption/redeem": {
      "post": {
        "tags": [
          "Redemption"
        ],
        "summary": "Redeem value, or trigger a promo",
        "description": "For a **value voucher** the response carries `kind=payment` (or\n`discount`) and the new balance; for a **promo voucher** the\nresponse carries `kind=promo` plus the promo descriptor — the POS\nuses `kind` to decide whether to book a tender line or apply a\ndiscount.\n\nReplay protection is keyed on the `Idempotency-Key` header per\nvoucher: a second call with the same key returns the original\nresult and does not act again.\n\nAccepts either `code` or `voucher_handle` (exactly one). On the\nhandle path the customer has already been identified by their\nloyalty card; the store-binding + TTL of the handle stand in for\nthe secret-equivalent protection a plain code requires.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RedeemRequest"
              },
              "examples": {
                "partial-value": {
                  "summary": "POS — redeem €30 of a value voucher",
                  "value": {
                    "code": "A5F3BBFF967CD2F3",
                    "amount": {
                      "amount": 3000,
                      "currency": "EUR"
                    },
                    "pos_id": "POS-03"
                  }
                },
                "ecom-value": {
                  "summary": "eCom — redeem €15 of a value voucher (PIN required)",
                  "value": {
                    "code": "A5F3BBFF967CD2F3",
                    "pin": "48213",
                    "amount": {
                      "amount": 1500,
                      "currency": "EUR"
                    }
                  }
                },
                "promo": {
                  "summary": "Trigger a promo voucher (no amount)",
                  "value": {
                    "code": "A5F3BBFF967CD2F3",
                    "pos_id": "POS-03"
                  }
                },
                "handle": {
                  "summary": "Redeem €20 via customer-bound handle",
                  "value": {
                    "voucher_handle": "vh_8f3c…",
                    "amount": {
                      "amount": 2000,
                      "currency": "EUR"
                    },
                    "pos_id": "POS-03"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Redemption recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RedeemResponse"
                },
                "examples": {
                  "value": {
                    "summary": "Value voucher",
                    "value": {
                      "txn_id": "txn_7f31",
                      "voucher_ref": "9d2f4a6f12c34c0eaba33d8c7b27cd11",
                      "kind": "payment",
                      "redeemed": {
                        "amount": 3000,
                        "currency": "EUR"
                      },
                      "balance_after": {
                        "amount": 2000,
                        "currency": "EUR"
                      },
                      "voucher_state": "partially_redeemed",
                      "masked_code": "XXXXXXXXXXXXD2F3"
                    }
                  },
                  "promo": {
                    "summary": "Promo voucher",
                    "value": {
                      "txn_id": "txn_88a0",
                      "voucher_ref": "1a7cc88e91a04f5b9d6c8e72fa2b1c44",
                      "kind": "promo",
                      "promo": {
                        "promo_id": "XMAS-3FOR2"
                      },
                      "voucher_state": "exhausted"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "$ref": "#/components/responses/PinFault"
          },
          "404": {
            "$ref": "#/components/responses/VoucherNotFound"
          },
          "409": {
            "$ref": "#/components/responses/StateConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/redemption/refund": {
      "post": {
        "tags": [
          "Redemption"
        ],
        "summary": "Reverse a redemption onto the original voucher",
        "description": "Re-credits the refund amount onto the voucher the value was\nredeemed from, referencing the original redemption via\n`original_txn_id`. Several refunds may reverse one redemption;\nthe total cannot exceed the redemption's unrefunded remainder.\nOmit `amount` to refund the whole remainder. The voucher is\nidentified by `original_txn_id`, so no `code` or `voucher_handle`\nis required here.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefundRequest"
              },
              "examples": {
                "partial": {
                  "value": {
                    "original_txn_id": "txn_7f31",
                    "amount": {
                      "amount": 1500,
                      "currency": "EUR"
                    },
                    "pos_id": "POS-03",
                    "reason": "return"
                  }
                },
                "full": {
                  "summary": "Omit amount to refund the whole remainder",
                  "value": {
                    "original_txn_id": "txn_7f31",
                    "reason": "return"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Refund recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefundResponse"
                },
                "examples": {
                  "default": {
                    "value": {
                      "txn_id": "txn_9c54",
                      "voucher_ref": "9d2f4a6f12c34c0eaba33d8c7b27cd11",
                      "refunded": {
                        "amount": 3000,
                        "currency": "EUR"
                      },
                      "balance_after": {
                        "amount": 5000,
                        "currency": "EUR"
                      },
                      "voucher_state": "partially_redeemed"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The `original_txn_id` is unknown or not a refundable\nredemption (loads, refunds, and promo triggers are not\nrefundable). The body is deliberately minimal so the endpoint\ncannot enumerate real txn ids.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": {
                        "code": "txn_not_found",
                        "message": "no transaction with that id"
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The refund would exceed the redemption's unrefunded remainder.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": {
                        "code": "refund_exceeds_original",
                        "message": "the refund exceeds the redemption's unrefunded remainder (1500)"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/redemption/customer-vouchers": {
      "post": {
        "tags": [
          "Customer-bound"
        ],
        "summary": "Look up a customer's bound vouchers by loyalty-card number",
        "description": "Resolves a scanned loyalty card to the customer's bound vouchers\nand mints one **ephemeral voucher handle** per voucher. The\nhandle is shown exactly once, store-bound to the calling client,\nand expires after `handles_expire_at`. Use it as `voucher_handle`\non subsequent check / load / redeem calls.\n\nPrivacy: a forgotten account, an unknown card, and an inactive\ncard all return the same 404 voucher_not_found shape — the\nendpoint cannot be used to enumerate or to detect a GDPR-forget.\n\nCross-country: when both the calling client and the customer\ncarry a country, they must match; otherwise the lookup is\nrefused with `country_not_allowed`.\n\nWhen the card resolves to a corporate (Firma / Verein) account\nthe response carries a `corporate` envelope with the firm's\nactive condition rules — the till's basket-level promo engine\nconsumes these without a separate round-trip.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerVouchersRequest"
              },
              "examples": {
                "pos": {
                  "value": {
                    "card_number": "LC-92F1-44A3",
                    "pos_id": "POS-03"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bound vouchers (possibly empty).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerVouchersResponse"
                },
                "examples": {
                  "value": {
                    "value": {
                      "account": {
                        "status": "active",
                        "country": "AT",
                        "account_type": "private"
                      },
                      "vouchers": [
                        {
                          "voucher_ref": "9d2f4a6f12c34c0eaba33d8c7b27cd11",
                          "kind": "payment",
                          "state": "sold",
                          "balance": {
                            "amount": 5000,
                            "currency": "EUR"
                          },
                          "handle": "vh_8f3c…64hex…"
                        }
                      ],
                      "handles_expire_at": "2026-05-25T14:52:11Z"
                    }
                  },
                  "corporate": {
                    "summary": "Loyalty card belongs to a Firma account",
                    "value": {
                      "account": {
                        "status": "active",
                        "country": "AT",
                        "account_type": "corporate"
                      },
                      "corporate": {
                        "id": "1a7cc88e91a04f5b9d6c8e72fa2b1c44",
                        "kind": "firma",
                        "name": "Hervis AT",
                        "conditions": [
                          {
                            "kind": "basket_percent_off",
                            "params": {
                              "percent": 5
                            }
                          }
                        ]
                      },
                      "vouchers": [],
                      "handles_expire_at": "2026-05-25T14:52:11Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No bound vouchers — either the card is unknown, the\naccount is forgotten, or the card is inactive. All three\ncollapse to the same shape to keep the endpoint from\nbeing usable as an oracle.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": {
                        "code": "voucher_not_found",
                        "message": "no voucher matches that card"
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The calling client's country does not match the customer's.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": {
                        "code": "country_not_allowed",
                        "message": "this customer cannot redeem in the calling client's country"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/redemption/refund-to-credit": {
      "post": {
        "tags": [
          "Customer-bound"
        ],
        "summary": "Refund a redemption onto a fresh bound voucher (customer credit)",
        "description": "Alternative refund settlement: instead of crediting the original\nvoucher, the refund lands on a freshly issued bound voucher\ndrawn from the `is_refund_credit` pool and assigned to the\nidentified customer. The original voucher's ledger is properly\nreversed for audit + double-spend protection; the customer\nreceives a new voucher_ref + balance to show on the receipt.\n\nRequires the customer to be identified by `customer_account_id`\n— typically obtained from the prior customer-vouchers lookup.\nIdempotent on `Idempotency-Key`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefundToCreditRequest"
              },
              "examples": {
                "partial": {
                  "value": {
                    "original_txn_id": "txn_7f31",
                    "amount": {
                      "amount": 1500,
                      "currency": "EUR"
                    },
                    "customer_account_id": "9b2c1d4e5f607182a3b4c5d6e7f80910",
                    "pos_id": "POS-03",
                    "reason": "exchange"
                  }
                },
                "full": {
                  "summary": "Omit amount to refund the unrefunded remainder",
                  "value": {
                    "original_txn_id": "txn_7f31",
                    "customer_account_id": "9b2c1d4e5f607182a3b4c5d6e7f80910",
                    "reason": "exchange"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Refund settled onto a fresh bound voucher.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefundToCreditResponse"
                },
                "examples": {
                  "default": {
                    "value": {
                      "txn_id": "txn_b021",
                      "new_voucher_ref": "11220033445566778899aabbccddeeff",
                      "original_voucher_ref": "9d2f4a6f12c34c0eaba33d8c7b27cd11",
                      "refunded": {
                        "amount": 1500,
                        "currency": "EUR"
                      },
                      "balance_after": {
                        "amount": 1500,
                        "currency": "EUR"
                      },
                      "new_voucher_state": "sold"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The `original_txn_id` or `customer_account_id` is unknown.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "409": {
            "description": "The refund would exceed the redemption's unrefunded\nremainder, or no refund-credit voucher is available in the\npool, or the customer is forgotten / inactive.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "webhooks": {
    "voucher.loaded": {
      "post": {
        "tags": [
          "Redemption"
        ],
        "summary": "A voucher was sold or recharged",
        "description": "Delivered after every successful `load`. Subscribers must\nverify the signature (see `WebhookEnvelope`) and return any\n2xx within a few seconds; non-2xx triggers retry with\nexponential backoff (1m / 5m / 15m / 1h / 4h, dead-letter\nafter 6 attempts).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              },
              "examples": {
                "default": {
                  "value": {
                    "delivery_id": "5b8e9f3e-9c2c-4d3a-8a1d-7a6b5c4d3e2f",
                    "event_type": "voucher.loaded",
                    "occurred_at": "2026-05-25T14:32:11Z",
                    "audit_event_id": 184213,
                    "data": {
                      "voucher_ref": "9d2f4a6f12c34c0eaba33d8c7b27cd11",
                      "txn_id": "txn_42f9",
                      "amount": {
                        "amount": 5000,
                        "currency": "EUR"
                      },
                      "balance_after": {
                        "amount": 5000,
                        "currency": "EUR"
                      },
                      "channel": "pos",
                      "store_id": "ST-001"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Subscriber accepted the event."
          },
          "default": {
            "description": "Non-2xx triggers retry per backoff schedule."
          }
        }
      }
    },
    "voucher.redeemed": {
      "post": {
        "tags": [
          "Redemption"
        ],
        "summary": "A voucher was redeemed (value or promo)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "OK."
          }
        }
      }
    },
    "voucher.refunded": {
      "post": {
        "tags": [
          "Redemption"
        ],
        "summary": "A refund was recorded on a voucher",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "OK."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "The redemption JWT obtained from `POST /v1/auth/token`.\nShort-lived (15 minutes by default), bound to the client's\nchannel and store. Send as `Authorization: Bearer <token>` on\nevery call.\n"
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "A client-chosen UUID. Replaying the same key on the same\nvoucher returns the original result and does not act twice.\n",
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 80,
          "example": "5b8e9f3e-9c2c-4d3a-8a1d-7a6b5c4d3e2f"
        }
      }
    },
    "schemas": {
      "Money": {
        "type": "object",
        "required": [
          "amount",
          "currency"
        ],
        "properties": {
          "amount": {
            "type": "integer",
            "format": "int64",
            "description": "Minor units (e.g. cents). 5000 = €50.00.",
            "example": 5000
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code. EUR only at launch.",
            "example": "EUR"
          }
        }
      },
      "Voucher": {
        "type": "object",
        "properties": {
          "voucher_ref": {
            "type": "string",
            "description": "Opaque, non-secret hex handle for the voucher (admin/display use). Never used as a redemption input.",
            "example": "9d2f4a6f12c34c0eaba33d8c7b27cd11"
          }
        }
      },
      "ErrorBody": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine error code. See the Errors page.",
                "example": "voucher_not_found"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "TokenRequest": {
        "type": "object",
        "required": [
          "api_key"
        ],
        "properties": {
          "api_key": {
            "type": "string",
            "description": "The plaintext API key shown once when the admin created the client."
          }
        }
      },
      "TokenResponse": {
        "type": "object",
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "channel"
        ],
        "properties": {
          "access_token": {
            "type": "string"
          },
          "token_type": {
            "type": "string",
            "enum": [
              "Bearer"
            ]
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds until expiry."
          },
          "channel": {
            "type": "string",
            "enum": [
              "pos",
              "ecom",
              "crm"
            ]
          },
          "store_id": {
            "type": "string",
            "description": "Present for POS clients."
          },
          "country": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2; present when the client is country-bound."
          }
        }
      },
      "CheckRequest": {
        "type": "object",
        "description": "Exactly one of `code` / `voucher_handle` must be supplied.\nMixing both yields `invalid_request`.\n",
        "properties": {
          "code": {
            "type": "string",
            "description": "The plain voucher code as scanned. Treat as a secret — see\nthe Code-vs-handle page on the developer portal.\n"
          },
          "voucher_handle": {
            "type": "string",
            "description": "An ephemeral handle returned by `customer-vouchers`. Opaque,\nstore-bound, ~20-minute TTL. Mutually exclusive with `code`.\n"
          },
          "pin": {
            "type": "string",
            "description": "Required when the JWT's channel is `ecom` and the code path is used."
          },
          "lock_minutes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 60,
            "description": "POS only. Place a store-scoped check-lock for the given\nminutes. Other stores and eCom are refused redemption on\nthe voucher until the lock expires; this store is\nunaffected.\n"
          }
        }
      },
      "CheckResponse": {
        "type": "object",
        "required": [
          "voucher_ref",
          "kind",
          "state"
        ],
        "properties": {
          "voucher_ref": {
            "$ref": "#/components/schemas/Voucher/properties/voucher_ref"
          },
          "kind": {
            "type": "string",
            "enum": [
              "payment",
              "discount",
              "promo"
            ],
            "description": "`payment`/`discount` for value vouchers (the POS books a\ntender line vs. a discount accordingly); `promo` for a\npromotional voucher (POS applies the campaign).\n"
          },
          "state": {
            "type": "string",
            "enum": [
              "generated",
              "active",
              "sold",
              "partially_redeemed",
              "empty",
              "exhausted",
              "expired",
              "blocked",
              "revoked",
              "lost_stolen"
            ]
          },
          "balance": {
            "$ref": "#/components/schemas/Money",
            "description": "Present for value vouchers."
          },
          "valid_from": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "valid_to": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "locked_until": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When a store-scoped check-lock is in force."
          }
        }
      },
      "LoadRequest": {
        "type": "object",
        "description": "Exactly one of `code` / `voucher_handle` must be supplied.\n",
        "properties": {
          "code": {
            "type": "string"
          },
          "voucher_handle": {
            "type": "string",
            "description": "Ephemeral customer-bound handle; mutually exclusive with `code`."
          },
          "amount": {
            "$ref": "#/components/schemas/Money",
            "description": "Required for flexible-value vouchers and for every recharge.\nFor a fixed-denomination initial sale, may be omitted (the\ndenomination is used) or must equal the denomination exactly.\n"
          },
          "pos_id": {
            "type": "string"
          }
        }
      },
      "LoadResponse": {
        "type": "object",
        "required": [
          "txn_id",
          "voucher_ref",
          "kind",
          "loaded",
          "balance_after",
          "voucher_state"
        ],
        "properties": {
          "txn_id": {
            "type": "string",
            "example": "txn_42f9",
            "description": "Ledger entry id in `txn_<hex>` form."
          },
          "voucher_ref": {
            "$ref": "#/components/schemas/Voucher/properties/voucher_ref"
          },
          "kind": {
            "type": "string",
            "enum": [
              "payment",
              "discount"
            ]
          },
          "loaded": {
            "$ref": "#/components/schemas/Money"
          },
          "balance_after": {
            "$ref": "#/components/schemas/Money"
          },
          "voucher_state": {
            "type": "string"
          },
          "masked_code": {
            "type": "string",
            "description": "Canonical voucher code with all but the last 4 characters\nreplaced by `X` — suitable to print on a receipt next to\n`txn_id`. **Empty on the handle path** (no plaintext code\nis materialised). Print the `voucher_ref` instead in that\ncase.\n",
            "example": "XXXXXXXXXXXXD2F3"
          }
        }
      },
      "RedeemRequest": {
        "type": "object",
        "description": "Exactly one of `code` / `voucher_handle` must be supplied.\n",
        "properties": {
          "code": {
            "type": "string"
          },
          "voucher_handle": {
            "type": "string",
            "description": "Ephemeral customer-bound handle; mutually exclusive with `code`."
          },
          "pin": {
            "type": "string",
            "description": "Required when the JWT's channel is `ecom` and the code path is used."
          },
          "amount": {
            "$ref": "#/components/schemas/Money",
            "description": "Required for value vouchers; omitted for promo triggers."
          },
          "pos_id": {
            "type": "string"
          }
        }
      },
      "RedeemResponse": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/RedeemValueResponse"
          },
          {
            "$ref": "#/components/schemas/RedeemPromoResponse"
          }
        ],
        "discriminator": {
          "propertyName": "kind",
          "mapping": {
            "payment": "#/components/schemas/RedeemValueResponse",
            "discount": "#/components/schemas/RedeemValueResponse",
            "promo": "#/components/schemas/RedeemPromoResponse"
          }
        }
      },
      "RedeemValueResponse": {
        "type": "object",
        "required": [
          "txn_id",
          "voucher_ref",
          "kind",
          "redeemed",
          "balance_after",
          "voucher_state"
        ],
        "properties": {
          "txn_id": {
            "type": "string"
          },
          "voucher_ref": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "payment",
              "discount"
            ]
          },
          "redeemed": {
            "$ref": "#/components/schemas/Money"
          },
          "balance_after": {
            "$ref": "#/components/schemas/Money"
          },
          "voucher_state": {
            "type": "string"
          },
          "masked_code": {
            "type": "string",
            "description": "Canonical voucher code with all but the last 4 characters\nreplaced by `X`. Empty on the handle path.\n",
            "example": "XXXXXXXXXXXXD2F3"
          }
        }
      },
      "RedeemPromoResponse": {
        "type": "object",
        "required": [
          "txn_id",
          "voucher_ref",
          "kind",
          "promo",
          "voucher_state"
        ],
        "properties": {
          "txn_id": {
            "type": "string"
          },
          "voucher_ref": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "promo"
            ]
          },
          "promo": {
            "type": "object",
            "required": [
              "promo_id"
            ],
            "properties": {
              "promo_id": {
                "type": "string"
              }
            }
          },
          "voucher_state": {
            "type": "string"
          },
          "masked_code": {
            "type": "string",
            "description": "As on RedeemValueResponse. Empty on the handle path.",
            "example": "XXXXXXXXXXXXD2F3"
          }
        }
      },
      "RefundRequest": {
        "type": "object",
        "required": [
          "original_txn_id"
        ],
        "properties": {
          "original_txn_id": {
            "type": "string",
            "description": "The redemption's `txn_id` from the original Redeem response."
          },
          "amount": {
            "$ref": "#/components/schemas/Money",
            "description": "Omit to refund the whole unrefunded remainder."
          },
          "pos_id": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "description": "Free-text; audited."
          }
        }
      },
      "RefundResponse": {
        "type": "object",
        "required": [
          "txn_id",
          "voucher_ref",
          "refunded",
          "balance_after",
          "voucher_state"
        ],
        "properties": {
          "txn_id": {
            "type": "string"
          },
          "voucher_ref": {
            "type": "string"
          },
          "refunded": {
            "$ref": "#/components/schemas/Money"
          },
          "balance_after": {
            "$ref": "#/components/schemas/Money"
          },
          "voucher_state": {
            "type": "string"
          }
        }
      },
      "CustomerVouchersRequest": {
        "type": "object",
        "required": [
          "card_number"
        ],
        "properties": {
          "card_number": {
            "type": "string",
            "description": "The scanned loyalty-card number. Server canonicalises and\nHMACs internally — variants of the same card (with or\nwithout grouping dashes) all collide on storage.\n"
          },
          "store_id": {
            "type": "string",
            "description": "Falls back to the JWT's store_id when omitted."
          },
          "pos_id": {
            "type": "string"
          }
        }
      },
      "CustomerVouchersResponse": {
        "type": "object",
        "required": [
          "account",
          "vouchers",
          "handles_expire_at"
        ],
        "properties": {
          "account": {
            "type": "object",
            "required": [
              "status"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "inactive"
                ]
              },
              "country": {
                "type": "string",
                "description": "ISO 3166-1 alpha-2."
              },
              "account_type": {
                "type": "string",
                "enum": [
                  "private",
                  "employee",
                  "corporate"
                ]
              }
            }
          },
          "corporate": {
            "type": "object",
            "description": "Present when account_type=corporate.",
            "required": [
              "id",
              "kind",
              "name",
              "conditions"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "firma",
                  "verein"
                ]
              },
              "name": {
                "type": "string"
              },
              "conditions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "kind",
                    "params"
                  ],
                  "properties": {
                    "kind": {
                      "type": "string",
                      "description": "e.g. basket_percent_off"
                    },
                    "params": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "vouchers": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "voucher_ref",
                "kind",
                "state",
                "handle"
              ],
              "properties": {
                "voucher_ref": {
                  "type": "string"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "payment",
                    "discount",
                    "promo"
                  ]
                },
                "state": {
                  "type": "string"
                },
                "balance": {
                  "$ref": "#/components/schemas/Money",
                  "description": "Present for value vouchers."
                },
                "valid_from": {
                  "type": "string",
                  "format": "date-time"
                },
                "valid_to": {
                  "type": "string",
                  "format": "date-time"
                },
                "handle": {
                  "type": "string",
                  "description": "The ephemeral handle (~32-char `vh_<hex>`) to pass as\n`voucher_handle` on subsequent check / load / redeem.\nShown once; not stored plaintext.\n"
                }
              }
            }
          },
          "handles_expire_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the handles in this response cease to be redeemable."
          }
        }
      },
      "RefundToCreditRequest": {
        "type": "object",
        "required": [
          "original_txn_id",
          "customer_account_id"
        ],
        "properties": {
          "original_txn_id": {
            "type": "string"
          },
          "amount": {
            "$ref": "#/components/schemas/Money",
            "description": "Omit to refund the unrefunded remainder."
          },
          "customer_account_id": {
            "type": "string",
            "description": "The hex customer-account id obtained from the customer-vouchers\nresponse or a CRM-supplied lookup.\n"
          },
          "pos_id": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          }
        }
      },
      "RefundToCreditResponse": {
        "type": "object",
        "required": [
          "txn_id",
          "new_voucher_ref",
          "original_voucher_ref",
          "refunded",
          "balance_after",
          "new_voucher_state"
        ],
        "properties": {
          "txn_id": {
            "type": "string"
          },
          "new_voucher_ref": {
            "type": "string",
            "description": "The fresh bound voucher that received the refund credit."
          },
          "original_voucher_ref": {
            "type": "string",
            "description": "The voucher the original redemption was drawn from."
          },
          "refunded": {
            "$ref": "#/components/schemas/Money"
          },
          "balance_after": {
            "$ref": "#/components/schemas/Money"
          },
          "new_voucher_state": {
            "type": "string"
          }
        }
      },
      "WebhookEnvelope": {
        "type": "object",
        "description": "The body of every webhook POST. The signature header\n`X-Webhook-Signature: sha256=<hex>` is the HMAC-SHA256 of the\nraw body bytes with the subscription's secret as the key —\ncompute it on receipt and compare in constant time.\n\nThe `delivery_id` is stable across retries — subscribers MUST\ndedupe on it. `audit_event_id` is the source-of-truth id in\nthe voucher service's hash-chained audit log; useful for\nout-of-band reconciliation.\n",
        "required": [
          "delivery_id",
          "event_type",
          "occurred_at",
          "audit_event_id",
          "data"
        ],
        "properties": {
          "delivery_id": {
            "type": "string",
            "format": "uuid",
            "description": "Stable identifier for this delivery; survives retries."
          },
          "event_type": {
            "type": "string",
            "description": "One of `voucher.load`, `voucher.redeem`, `voucher.refund`,\n`voucher.adjust`, `voucher.revive`, `voucher.check`,\n`customer.assign_voucher`, plus future `voucher.*` event\ntypes.\n",
            "example": "voucher.redeem"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "audit_event_id": {
            "type": "integer",
            "format": "int64"
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "Event-specific payload. Shape varies by `event_type` — see\nthe developer portal for per-event schemas.\n"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request body or headers are not well-formed (missing `code`\n/ `voucher_handle`, both supplied, missing `Idempotency-Key`,\nmalformed money object, …).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "missing-idempotency": {
                "value": {
                  "error": {
                    "code": "invalid_request",
                    "message": "Idempotency-Key header is required"
                  }
                }
              },
              "both-inputs": {
                "value": {
                  "error": {
                    "code": "invalid_request",
                    "message": "provide either code or voucher_handle, not both"
                  }
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, invalid, or expired JWT.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "default": {
                "value": {
                  "error": {
                    "code": "unauthenticated",
                    "message": "unauthenticated"
                  }
                }
              }
            }
          }
        }
      },
      "VoucherNotFound": {
        "description": "No voucher matches the supplied code or handle. The body is\ndeliberately minimal so the endpoint cannot be used to\nenumerate live codes / handles.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "code": {
                "value": {
                  "error": {
                    "code": "voucher_not_found",
                    "message": "no voucher matches this code"
                  }
                }
              },
              "handle": {
                "value": {
                  "error": {
                    "code": "voucher_handle_invalid",
                    "message": "no voucher matches this handle"
                  }
                }
              }
            }
          }
        }
      },
      "InsufficientBalance": {
        "description": "The voucher's balance is less than the requested redemption\namount.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "default": {
                "value": {
                  "error": {
                    "code": "insufficient_balance",
                    "message": "the voucher balance (1500) is less than the requested amount (3000)"
                  }
                }
              }
            }
          }
        }
      },
      "PinFault": {
        "description": "The eCom call needs a PIN, gave a wrong PIN, or the voucher's\nPIN is locked (5 wrong attempts → 15-minute lockout).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "pin-required": {
                "value": {
                  "error": {
                    "code": "pin_required",
                    "message": "a PIN is required to use this voucher online"
                  }
                }
              },
              "pin-invalid": {
                "value": {
                  "error": {
                    "code": "pin_invalid",
                    "message": "incorrect PIN; 2 attempt(s) remaining"
                  }
                }
              },
              "pin-locked": {
                "value": {
                  "error": {
                    "code": "pin_locked",
                    "message": "too many incorrect PIN attempts; the voucher PIN is locked"
                  }
                }
              }
            }
          }
        }
      },
      "StateConflict": {
        "description": "The voucher's state prevents the requested action. Codes:\n`voucher_not_active`, `voucher_expired`, `voucher_revoked`,\n`voucher_blocked`, `voucher_locked`, `voucher_invalid`,\n`channel_not_allowed`, `recharge_not_allowed`,\n`value_cap_reached`, `promo_already_used`, `promo_cap_reached`,\n`refund_exceeds_original`, `currency_mismatch`,\n`idempotency_conflict`, `country_not_allowed`.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "voucher-locked": {
                "value": {
                  "error": {
                    "code": "voucher_locked",
                    "message": "this voucher is temporarily locked to another store"
                  }
                }
              },
              "value-cap": {
                "value": {
                  "error": {
                    "code": "value_cap_reached",
                    "message": "this redemption would exceed the voucher's value cap (8000 of 8000 already used)"
                  }
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. The `Retry-After` header gives a hint.\n",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait before retrying."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "default": {
                "value": {
                  "error": {
                    "code": "rate_limited",
                    "message": "too many requests; retry later"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}