openapi: 3.0.3 info: title: Kupon Plus Merchant API version: "2.0" description: | The Kupon Plus Merchant API lets a merchant's system issue vouchers, cancel them and download a daily report. This document describes the calls exactly as the API answers them today. ## Before the first call You get three things from the owner of the dataset: - **dataset** – the first part of every path, for example `sandbox` for the shared test dataset; - **X-API-TOKEN** – identifies your merchant account; send it with every call; - **X-DATA-TOKEN** – the key that decrypts vouchers; send it with `getVoucherByValue`. Your account may be limited to calls from certain IP addresses. Call `ping/200` first: it needs no token and returns the address the server sees for you (`data.myIP`). That is the address the dataset owner has to allow. ## Reading a response Every call except a successful `report:json1` returns the same envelope: ```json {"error": {"code": 200, "text": "OK"}, "data": { }} ``` - **Decide by `error.code`, not by the HTTP status alone.** Some refusals keep HTTP 200 and carry the real result in `error.code`: a call from an IP address that is not allowed (`getVoucherByValue`, `cancelVoucher`) answers HTTP 200 with `error.code` 401, and a database error while issuing or cancelling answers HTTP 200 with `error.code` 500. - The human-readable reason is in `error.text` or, for some errors, in `error.content`. The number in brackets, for example `(10)`, identifies the place of the refusal; quote it when you contact support. - **HTTP 200 from `getVoucherByValue` is not a voucher.** A voucher was issued only when `data.voucher` is present and not empty. ## Common refusals | HTTP | error.code | text | meaning | |---|---|---|---| | 401 | 401 | `Unauthorized access (0)` | X-API-TOKEN missing, unknown or expired | | 401 | 401 | `Unauthorized access to dataset (10)` | the dataset in the path is not the dataset of your token | | 401 | 401 | `Unauthorized access (00)`, `(000)` | your account may not call this endpoint or method | | 200 or 401 | 401 | `Unauthorized access (0000)` | the call came from an IP address your account does not allow | ## Test cases before go-live Every call below lists its **test cases**: what happens in your system, what to think about in your code, the call to make and the result you must get. Cases marked *required* must pass before you go live. **Run this case** fills in the call; in the `sandbox` dataset it also compares the answer with the expected result. ## Sandbox The `sandbox` dataset is shared for testing. Vouchers issued there are test vouchers. Which voucher values it holds depends on what was uploaded to it; ask the dataset owner if a call returns nothing. In your own dataset every call is real: `getVoucherByValue` issues a real voucher and `cancelVoucher` cancels it, also when you send it from this page. servers: - url: https://kupon.plus/api/{dataset} description: Kupon Plus variables: dataset: default: sandbox description: Your dataset; `sandbox` is the shared test dataset. x-test-values: [sandbox, sendbox] x-result-hints: - when: {code: 401} text: "Refused with error.code 401 inside HTTP {http}: this IP address is not allowed for your account. Run ping/200 and send data.myIP to the dataset owner." tags: - name: Vouchers description: Issue and cancel vouchers. - name: Reports description: Daily report of issued and cancelled vouchers. - name: Connection description: Test the connection and your IP address. paths: /getVoucherByValue/{value}: parameters: - $ref: "#/components/parameters/value" get: tags: [Vouchers] operationId: getVoucherByValue x-live-warning: issues a real voucher x-result-hints: - when: {code: 200, missing: [data.voucher]} text: "No voucher was issued: there is no voucher of this value in the dataset, or X-DATA-TOKEN does not match. HTTP 200 alone is not a voucher." summary: Issue one voucher of the given value description: | Issues one voucher of the requested value and marks it as used by your merchant account. Of the vouchers that are still valid long enough (the dataset sets the minimum, three months by default), the one that expires first is issued. **An empty result is also HTTP 200.** When no voucher is issued, `error.code` is `200` and `data` contains only `metadata: null`; `vid` and `voucher` are missing. Nothing is issued and nothing is deducted. This happens when there is no voucher of that value and also when X-DATA-TOKEN is missing or wrong, so an empty result alone does not prove that the stock is empty. Never treat HTTP 200 as "voucher received": **if `data.voucher` is missing or empty, do not complete the transaction.** Stock is replenished by uploading new vouchers, not by repeating the call. Use `POST` to store your own data (transaction ID, shop, terminal) with the voucher. x-test-cases: - id: V-01 title: Issue a voucher at the till required: true when: The customer pays for a voucher and your till asks Kupon Plus for one. consider: | Complete the sale only when `data.voucher` is present and not empty. Print or show `data.voucher` to the customer and store `data.vid` with your transaction: you need it to cancel the voucher later. test: params: {value: "10"} expect: http: 200 code: 200 present: [data.vid, data.voucher, data.expir8601] result: The answer carries `data.vid` and a non-empty `data.voucher`. - id: V-02 title: No voucher of that value required: true when: You ask for a value the dataset has no voucher for. consider: | HTTP 200 is not a voucher. Your till must not complete the sale and must tell the cashier that the voucher is not available. Do not repeat the call in a loop: stock is replenished by uploading vouchers. test: params: {value: "3"} expect: http: 200 code: 200 missing: [data.vid, data.voucher] result: HTTP 200 with `error.code` 200, but no `data.vid` and no `data.voucher`. - id: V-03 title: Wrong X-DATA-TOKEN required: true when: Your configuration holds a wrong or outdated X-DATA-TOKEN. consider: | The answer looks exactly like an empty stock. If your till gets only empty results, check the configuration before you report missing stock. test: params: {value: "10"} headers: {X-DATA-TOKEN: wrong-data-token} expect: http: 200 code: 200 missing: [data.vid, data.voucher] result: The same empty result as V-02; nothing is issued. - id: V-04 title: Calling again issues another voucher required: true when: Your till did not get an answer in time and wants to try again. consider: | The call is not idempotent: every successful call issues one more voucher. Repeat it only when you are sure the first call issued nothing, and cancel vouchers you will not hand out (C-01). test: params: {value: "10"} expect: http: 200 code: 200 present: [data.vid, data.voucher] differs: {data.vid: "{V-01.data.vid}"} result: A second voucher with a different `data.vid` than V-01. - id: V-05 title: Value that is not a number when: Your till sends the value with a currency sign or a comma. consider: | Send the value as a plain number with a dot, for example `5` or `0.5`. test: params: {value: "10EUR"} expect: http: 401 code: 401 contains: {error.content: "(11)"} result: Refused, the reason in `error.content` ends with `(11)`. - id: V-06 title: Missing or wrong X-API-TOKEN when: The token is missing, mistyped or expired. consider: | Show a configuration error, do not retry. Ask the dataset owner for a new token. test: params: {value: "10"} headers: {X-API-TOKEN: wrong-api-token} expect: http: 401 code: 401 contains: {error.text: "(0)"} result: HTTP 401, `Unauthorized access (0)`. - id: V-07 title: Call from an IP address that is not allowed when: Your account is limited to certain IP addresses and the call comes from another one. consider: | The refusal keeps HTTP 200. Decide by `error.code`: 401 means the call was refused and nothing was issued. Run `ping/200` from the same machine and send `data.myIP` to the dataset owner. setup: The dataset owner sets an IP rule on your test account that your address does not match. test: params: {value: "10"} expect: http: 200 code: 401 contains: {error.text: "(0000)"} missing: [data.vid, data.voucher] result: HTTP 200 with `error.code` 401 and `Unauthorized access (0000)`. security: - X-API-TOKEN: [] X-DATA-TOKEN: [] responses: "200": $ref: "#/components/responses/VoucherResult" "401": $ref: "#/components/responses/VoucherRefused" post: tags: [Vouchers] operationId: getVoucherByValueWithMetadata x-live-warning: issues a real voucher x-result-hints: - when: {code: 200, missing: [data.voucher]} text: "No voucher was issued: there is no voucher of this value in the dataset, or X-DATA-TOKEN does not match. HTTP 200 alone is not a voucher." summary: Issue one voucher and store your metadata with it description: | The same as `GET`, and the JSON object in the request body is stored with the issued voucher. It comes back in `data.metadata` and in the daily report, so it can identify the transaction in a claim. A body that is not valid JSON is stored as an empty object. When no voucher is issued, nothing is stored. x-test-cases: - id: M-01 title: Store your transaction with the voucher required: true when: You issue a voucher and want to find the sale again in a claim or in the daily report. consider: | Send your own transaction ID, shop and till in the body. The same object comes back in `data.metadata` and in the daily report (R-01). test: params: {value: "10"} body: {TxID: tx-0001, shop: S01, till: T1} expect: http: 200 code: 200 present: [data.vid, data.voucher] equals: {data.metadata: {TxID: tx-0001, shop: S01, till: T1}} result: A voucher, and `data.metadata` equals the body you sent. security: - X-API-TOKEN: [] X-DATA-TOKEN: [] requestBody: required: false content: application/json: schema: type: object additionalProperties: true example: TxID: tx-0001 shop: S01 till: T1 responses: "200": $ref: "#/components/responses/VoucherResult" "401": $ref: "#/components/responses/VoucherRefused" /cancelVoucher/{voucherid}: put: tags: [Vouchers] operationId: cancelVoucher x-live-warning: cancels a real voucher summary: Cancel a voucher you were issued description: | Cancels a voucher that your merchant account was issued. The dataset owner is notified by e-mail. - `canceled: true` – the voucher is cancelled. Calling it again for the same voucher answers the same way. - `canceled: false` – nothing was cancelled: the voucher does not exist or was not issued to your account. x-test-cases: - id: C-01 title: Cancel a voucher you issued required: true when: The sale is voided or the customer returns the voucher. consider: | Cancel with the `data.vid` you stored at the sale. Mark the sale as cancelled only when `data.canceled` is `true`. test: params: {voucherid: "{V-01.data.vid}"} expect: http: 200 code: 200 equals: {data.canceled: true} result: "`data.canceled` is `true`." - id: C-02 title: Cancel the same voucher again required: true when: Your till did not get the answer to a cancel and sends it again. consider: | Repeating a cancel is safe: the answer is the same and nothing else changes. test: params: {voucherid: "{V-01.data.vid}"} expect: http: 200 code: 200 equals: {data.canceled: true} result: The same answer as C-01. - id: C-03 title: Cancel a voucher that is not yours required: true when: The voucher ID is mistyped or belongs to another merchant. consider: | `data.canceled` is `false`: nothing was cancelled. Do not refund the customer. test: params: {voucherid: "999"} expect: http: 200 code: 200 equals: {data.canceled: false} result: "`data.canceled` is `false`." - id: C-04 title: Voucher ID that is not a number when: Your till sends the voucher code instead of `data.vid`. consider: | Cancel by `data.vid`, never by the voucher code. test: params: {voucherid: "A1B2C3"} expect: http: 400 code: 400 contains: {error.text: "(12)"} result: HTTP 400, `Input parameter error (12)`. security: - X-API-TOKEN: [] parameters: - name: voucherid in: path required: true description: "`vid` returned by `getVoucherByValue`." schema: type: integer example: 1001 responses: "200": description: "Result. Check `error.code`: 401 means an IP address that is not allowed, 500 a database error (`Internal server error (11)`)." content: application/json: schema: type: object properties: error: $ref: "#/components/schemas/Error" data: type: object properties: vid: type: integer canceled: type: boolean examples: canceled: summary: Voucher cancelled value: error: {code: 200, text: OK} data: {vid: 1001, canceled: true} notYours: summary: Unknown voucher or not issued to you value: error: {code: 200, text: OK} data: {vid: "999", canceled: false} "400": description: "`voucherid` is not a number: `Input parameter error (12)`." content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": $ref: "#/components/responses/Unauthorized" /report:json1/{requested_date}: get: tags: [Reports] operationId: reportJson1 summary: Daily report of your issued and cancelled vouchers description: | The report of one day, generated every night between 02:00 and 03:00 UTC. A day is available once its report has been generated; until then the call answers 404. **A successful response is the bare JSON array, without the envelope.** Errors use the envelope. - `status` – `S` sold (issued), `C` cancelled; - `vid` – voucher ID; `vvalue` – value; - `action_date` – when it happened, ISO 8601; - `metadata` – the object you sent with `POST getVoucherByValue`. The report must be enabled for your account by the dataset owner; otherwise 403. x-test-cases: - id: R-01 title: Read yesterday's report when: Your accounting compares its own sales with Kupon Plus once a day. consider: | Read the report after 03:00 UTC. A successful answer is a bare JSON array without the envelope. Match the rows to your sales by `vid` and by your `metadata`. setup: The dataset owner enables the report for your account. test: params: {requested_date: "{yesterday}"} expect: http: 200 array: true result: HTTP 200 and a JSON array (empty when nothing happened that day). - id: R-02 title: Report not generated yet when: You ask for a day whose report does not exist yet. consider: | 404 is not an error of your system. Try again later, not in a loop. setup: The dataset owner enables the report for your account. test: params: {requested_date: "{today}"} expect: http: 404 code: 404 contains: {error.content: "Report not available"} result: HTTP 404, `Report not available`. - id: R-03 title: Date in a wrong format when: Your system sends the date in a local format. consider: | Always send `YYYY-MM-DD`. test: params: {requested_date: "15.01.2030"} expect: http: 400 code: 400 result: HTTP 400 with the reason in `error.content`. security: - X-API-TOKEN: [] parameters: - name: requested_date in: path required: true schema: type: string format: date example: "2030-01-15" responses: "200": description: Report of the day. content: application/json: schema: type: array items: $ref: "#/components/schemas/ReportRow" example: - status: S vid: 1001 vvalue: "100" action_date: "2030-01-15T12:04:01+01:00" metadata: {TxID: tx-0001, shop: S01, till: T1} - status: C vid: 1002 vvalue: "100" action_date: "2030-01-15T12:04:02+01:00" metadata: {TxID: tx-0002, shop: S01, till: T1} "400": description: "Date missing or not `YYYY-MM-DD` (`(11)`, `(12)`, `(13)`), or the merchant does not exist." content: application/json: schema: $ref: "#/components/schemas/Envelope" "401": $ref: "#/components/responses/Unauthorized" "403": description: "`This export is not allowed` – the report is not enabled for your account." content: application/json: schema: $ref: "#/components/schemas/Envelope" "404": description: "`Report not available` – not generated yet, or no report for that day." content: application/json: schema: $ref: "#/components/schemas/Envelope" "500": description: "`Invalid format in report`." content: application/json: schema: $ref: "#/components/schemas/Envelope" /ping/{requested_response}: get: tags: [Connection] operationId: ping summary: Test the connection and see your IP address description: | Needs no token. `ping/200` returns the IP address the server sees for you in `data.myIP`; it is the address checked against your account's allowed IP addresses. `ping/400`, `ping/401` and `ping/500` return that status, so you can test how your system handles errors. x-test-cases: - id: P-01 title: Find the IP address you call from required: true when: You connect a new server or till to Kupon Plus. consider: | Call it from the machine that will make the real calls, not from your laptop. Send `data.myIP` to the dataset owner if your account is limited to IP addresses. test: params: {requested_response: "200"} expect: http: 200 code: 200 present: [data.myIP] result: "`data.myIP` holds the address the server sees." - id: P-02 title: Your system survives a server error required: true when: Kupon Plus answers with an error status. consider: | Your till must show an error, not complete the sale, and not retry endlessly. Try `ping/400` and `ping/401` as well. test: params: {requested_response: "500"} expect: http: 500 result: HTTP 500. security: [] parameters: - name: requested_response in: path required: true schema: type: string enum: ["200", "400", "401", "500"] responses: "200": description: Connection works. content: application/json: schema: type: object properties: error: $ref: "#/components/schemas/Error" data: type: object properties: myIP: type: string example: error: {code: 200, text: OK} data: {myIP: "203.0.113.7"} "400": description: Requested `ping/400`. "401": description: Requested `ping/401`. "500": description: Requested `ping/500`. components: securitySchemes: X-API-TOKEN: type: apiKey in: header name: X-API-TOKEN description: Token of your merchant account. X-DATA-TOKEN: type: apiKey in: header name: X-DATA-TOKEN description: Key that decrypts vouchers. Kupon Plus does not store it in readable form and cannot show it to you. parameters: value: name: value in: path required: true description: Voucher value, a number such as `5` or `0.5`. Which values exist depends on the vouchers uploaded to the dataset. schema: type: string example: "10" schemas: Error: type: object properties: code: type: integer description: The real result of the call. 200 means success. example: 200 text: type: string example: OK content: type: string description: Reason of some errors instead of `text`. Envelope: type: object properties: error: $ref: "#/components/schemas/Error" data: {} Voucher: type: object description: All fields except `metadata` are present only when a voucher was issued. properties: vid: type: integer description: Voucher ID; use it for `cancelVoucher`. voucher: type: string maxLength: 255 description: Decrypted voucher code. Check that it is present and not empty before completing the transaction. value: type: string currency: type: string metadata: type: object nullable: true description: Your stored object; `null` when no voucher was issued. expir: type: string description: Expiration, `YYYY-MM-DD HH:MM:SS` in UTC. expir8601: type: string format: date-time description: The same expiration in ISO 8601. ReportRow: type: object properties: status: type: string enum: [S, C] vid: type: integer vvalue: type: string action_date: type: string format: date-time metadata: type: object nullable: true responses: VoucherResult: description: "Result. **A voucher was issued only if `data.voucher` is present and not empty.** Check `error.code`: 401 means an IP address that is not allowed, 500 a database error (`Internal server error (12)`)." content: application/json: schema: type: object properties: error: $ref: "#/components/schemas/Error" data: $ref: "#/components/schemas/Voucher" examples: voucherIssued: summary: Voucher issued value: error: {code: 200, text: OK} data: vid: 1001 voucher: A1B2C3D4E5F6G7H8 value: "10" currency: EUR metadata: {TxID: tx-0001, shop: S01, till: T1} expir: "2030-12-31 00:00:00" expir8601: "2030-12-31T00:00:00Z" noVoucher: summary: No voucher issued (still HTTP 200) value: error: {code: 200, text: OK} data: {metadata: null} ipNotAllowed: summary: Call from an IP address that is not allowed (still HTTP 200) value: error: {code: 401, text: "Unauthorized access (0000)"} data: {} VoucherRefused: description: "Token or dataset refused (see the table in the introduction), or `value` is not a number: `Bad input parameters ('') (11)` in `error.content`." content: application/json: schema: $ref: "#/components/schemas/Envelope" Unauthorized: description: Token or dataset refused; see the table in the introduction. content: application/json: schema: $ref: "#/components/schemas/Envelope"