> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endl.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Retrieve vouchers

> The only endpoint that returns redemption material. It has its own scope (`vouchers:read`), its own tighter rate limit (**30/min**, a separate budget), and every retrieval is individually audited.

Retrieval is repeatable and returns the same material; the first retrieval is timestamped and later ones do not move it. This route is **replay-protected**, so each call needs a fresh signature.

**Retrieve once and store.** The tight limit exists because bulk retrieval is indistinguishable from exfiltration — fetch per order as it fulfils, never sweep historical orders. Never log `secret`; log `voucher_id` and `masked_hint` instead.

## Reading `secret`

`secret` is a JSON document **encoded as a string**. Parse it, then read the
fields named by `kind`.

| `kind`     | `code`          | `pin`    | `claimUrl`      | How the customer redeems             |
| ---------- | --------------- | -------- | --------------- | ------------------------------------ |
| `CODE`     | redemption code | —        | —               | Enters the code.                     |
| `CODE_PIN` | redemption code | required | —               | Enters the code and the PIN.         |
| `URL`      | —               | —        | redemption link | Opens the link.                      |
| `URL_PIN`  | —               | required | redemption link | Opens the link, then enters the PIN. |

```javascript theme={null}
const secret = JSON.parse(voucher.secret);
switch (secret.kind) {
  case 'CODE':     return { code: secret.code };
  case 'CODE_PIN': return { code: secret.code, pin: secret.pin };
  case 'URL':      return { url: secret.claimUrl };
  case 'URL_PIN':  return { url: secret.claimUrl, pin: secret.pin };
}
```

<Warning>
  **Do not assume `code` is always present.** On `URL` and `URL_PIN` the
  redemption material is a link and there is no code — rendering `secret.code`
  blindly shows the customer `undefined`.

  **Do not discard `pin`.** A `CODE_PIN` voucher is unredeemable without it, and
  the customer has no way to recover it.
</Warning>

## Availability

| Order state                | Response                             | What to do                                                         |
| -------------------------- | ------------------------------------ | ------------------------------------------------------------------ |
| `PENDING` / `UNDER_REVIEW` | `409 ORDER_NOT_IN_RETRIEVABLE_STATE` | Keep polling. This is the expected answer, not an error condition. |
| `FULFILLED`                | `200`                                | Read the vouchers.                                                 |
| `FAILED` / `REFUNDED`      | `409 ORDER_NOT_IN_RETRIEVABLE_STATE` | Never becomes retrievable.                                         |

## Handling rules

| Rule                                   | Detail                                                                                                                                              |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Retrieval is repeatable                | Fetching again returns the same material. The first retrieval is timestamped; later ones do not move it.                                            |
| Each retrieval needs a fresh signature | This route is replay-protected, so two identical retrievals inside the same second collide.                                                         |
| Retrieve once and store                | The tight limit exists because bulk retrieval is indistinguishable from exfiltration. Fetch per order as it fulfils; never sweep historical orders. |
| Never log `secret`                     | Log `voucher_id` and `masked_hint` instead. They identify the voucher without disclosing it.                                                        |


## OpenAPI

````yaml api-reference/endl-giftcard-api.json GET /orders/{order_id}/vouchers
openapi: 3.1.0
info:
  title: Endl Giftcard API
  version: '1'
  description: >-
    Order gift cards, poll them to completion, and retrieve the redemption
    material.


    Five endpoints, one signing scheme, and one rule that governs everything
    else: **an order is a fulfilment instruction, and it is accepted before it
    is finished.**


    This is a separate service from the Endl Partner API. It does not use
    `API-KEY`, it is not under `/api/v0`, and it does not take an `Api-Version`
    header. Requests are signed with HMAC-SHA256.
servers:
  - url: https://api-sandbox.endl.io/giftcards
    description: Sandbox — confirm the host with Endl
security: []
paths:
  /orders/{order_id}/vouchers:
    get:
      tags:
        - Vouchers
      summary: Retrieve vouchers
      description: >-
        The only endpoint that returns redemption material. It has its own scope
        (`vouchers:read`), its own tighter rate limit (**30/min**, a separate
        budget), and every retrieval is individually audited.


        Retrieval is repeatable and returns the same material; the first
        retrieval is timestamped and later ones do not move it. This route is
        **replay-protected**, so each call needs a fresh signature.


        **Retrieve once and store.** The tight limit exists because bulk
        retrieval is indistinguishable from exfiltration — fetch per order as it
        fulfils, never sweep historical orders. Never log `secret`; log
        `voucher_id` and `masked_hint` instead.
      parameters:
        - $ref: '#/components/parameters/Timestamp'
        - name: order_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The order id.
          example: 9f1c8a44-2b7e-4d31-9a6f-5c0e7b2d81a3
      responses:
        '200':
          description: The redemption material. Only items in a retrievable state appear.
          content:
            application/json:
              example:
                order_id: 9f1c8a44-2b7e-4d31-9a6f-5c0e7b2d81a3
                items:
                  - item_id: 2c81a0f4-9c33-4f02-b0b7-1d5e9f2a44c8
                    canonical_sku: BIGBASKET--IN--INR
                    vouchers:
                      - voucher_id: b1d7a9e0-6c2f-4f7a-9f2b-77a1c0de4411
                        seq: 1
                        kind: CODE_PIN
                        denomination_minor: '50000'
                        currency: INR
                        expires_at: '2027-09-21T00:00:00.000Z'
                        masked_hint: '****4821'
                        secret: >-
                          {"kind":"CODE_PIN","code":"BB-4821-9930-1174","pin":"4821"}
        '401':
          description: Missing headers, an unknown key id, or an inactive key.
          content:
            application/json:
              example:
                code: AUTH_INVALID_KEY
                message: credential could not be verified
        '403':
          description: Authenticated, but the key lacks the scope this route needs.
          content:
            application/json:
              example:
                code: SCOPE_FORBIDDEN
                message: key lacks the required scope
        '404':
          description: >-
            Unknown id, malformed id, or an order belonging to someone else —
            deliberately indistinguishable.
          content:
            application/json:
              example:
                code: ORDER_NOT_FOUND
                message: order not found
        '409':
          description: >-
            The order is not in a retrievable state. While `PENDING` or
            `UNDER_REVIEW` this is expected — keep polling.
          content:
            application/json:
              example:
                code: ORDER_NOT_IN_RETRIEVABLE_STATE
                message: order is not in a retrievable state
        '429':
          description: >-
            Rate limited. Honour the `Retry-After` header, in whole seconds,
            never below 1.
          content:
            application/json:
              example:
                code: RATE_LIMITED
                message: rate limit exceeded
        '500':
          description: Unexpected server error. Retry with exponential backoff.
          content:
            application/json:
              example:
                code: INTERNAL_ERROR
                message: unexpected error
      security:
        - apiKeyId: []
        - signature: []
components:
  parameters:
    Timestamp:
      name: X-Timestamp
      in: header
      required: false
      description: >-
        Unix seconds. Optional, carried for symmetry — send the same value as
        the `t=` inside `X-Signature`, which is the one actually validated.
      schema:
        type: string
      example: '1758470400'
  securitySchemes:
    apiKeyId:
      type: apiKey
      in: header
      name: X-Api-Key-Id
      description: >-
        Your key id, verbatim. Public — it identifies the credential. The secret
        is never transmitted.
    signature:
      type: apiKey
      in: header
      name: X-Signature
      description: >-
        `t=<unix-seconds>,v1=<hex64>` — HMAC-SHA256 over the canonical string.
        See [Signing requests](/giftcards/authentication).

````