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

# Redeem a voucher at an outlet

> Atomically validates the PIN, GPS distance, redemption limits, claim/membership requirements, and dispenses a voucher code (when the voucher is configured for codes). 

Rate limited to **5 attempts per user per 60 seconds** to deter PIN brute-forcing. The window is sliding; when exceeded the response is HTTP 429 with `retry-after` (seconds) header and `{ error: "rate_limited", details: { retryAfterSeconds } }`. 

GPS rules: if both `consumerLat` and `consumerLng` are supplied and the user is within 150m of the outlet, status is SUCCESS. If the user is further than 150m, the redemption is rejected with `too_far` (no row written). If the consumer omits GPS (both fields missing), status is FLAGGED with `flagReason="gps_unavailable"`. If the OUTLET has no coordinates, the geofence can't run — status stays SUCCESS but `flagReason="outlet_not_geolocated"` is set as an audit tag for admin follow-up.



## OpenAPI

````yaml /openapi.json post /me/redemptions
openapi: 3.1.0
info:
  title: Voucher Platform — Partner Integration API
  version: 1.0.0
  description: >-
    Server-to-server and consumer endpoints used by partner apps to provision
    users, embed the voucher webview, and receive lifecycle webhooks. See the
    [partner integration guide](/docs) for the full narrative including JWT
    setup, HMAC signing, and a worked end-to-end example.
servers:
  - url: http://localhost:4000
    description: Local development
security: []
tags:
  - name: Integrations
    description: >-
      Server-to-server endpoints called from the partner backend. Authenticated
      with an HMAC signature.
  - name: Consumer
    description: >-
      Called from inside the partner-issued webview (or partner-native UI) on
      behalf of an end user. Authenticated with a partner-signed JWT.
paths:
  /me/redemptions:
    post:
      tags:
        - Consumer
      summary: Redeem a voucher at an outlet
      description: >-
        Atomically validates the PIN, GPS distance, redemption limits,
        claim/membership requirements, and dispenses a voucher code (when the
        voucher is configured for codes). 


        Rate limited to **5 attempts per user per 60 seconds** to deter PIN
        brute-forcing. The window is sliding; when exceeded the response is HTTP
        429 with `retry-after` (seconds) header and `{ error: "rate_limited",
        details: { retryAfterSeconds } }`. 


        GPS rules: if both `consumerLat` and `consumerLng` are supplied and the
        user is within 150m of the outlet, status is SUCCESS. If the user is
        further than 150m, the redemption is rejected with `too_far` (no row
        written). If the consumer omits GPS (both fields missing), status is
        FLAGGED with `flagReason="gps_unavailable"`. If the OUTLET has no
        coordinates, the geofence can't run — status stays SUCCESS but
        `flagReason="outlet_not_geolocated"` is set as an audit tag for admin
        follow-up.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - voucherId
                - outletId
                - pin
              additionalProperties: false
              properties:
                voucherId:
                  type: string
                  format: uuid
                  description: The voucher being redeemed.
                outletId:
                  type: string
                  format: uuid
                  description: The outlet the user is physically at.
                pin:
                  type: string
                  pattern: ^\d{4}$
                  description: >-
                    4-digit PIN the outlet staff enters. Compared bcrypt-side;
                    case-sensitive.
                  example: '4829'
                consumerLat:
                  type: number
                  minimum: -90
                  maximum: 90
                  description: >-
                    Optional. Device latitude. Required to record
                    `status=SUCCESS`; otherwise the redemption is flagged.
                consumerLng:
                  type: number
                  minimum: -180
                  maximum: 180
                  description: >-
                    Optional. Device longitude. Must be provided together with
                    `consumerLat`.
                gpsAccuracyMeters:
                  type: number
                  minimum: 0
                  description: >-
                    Optional. Reported accuracy of the GPS fix; stored for
                    audit.
      responses:
        '201':
          description: Redemption committed.
          content:
            application/json:
              schema:
                description: Redemption committed.
                type: object
                additionalProperties: true
                properties:
                  id:
                    type: string
                    format: uuid
                  userId:
                    type: string
                    format: uuid
                  voucherId:
                    type: string
                    format: uuid
                  outletId:
                    type: string
                    format: uuid
                  cycleKey:
                    type: string
                    description: >-
                      Bucket key for the voucher's refresh cycle (e.g. `2026-05`
                      for monthly).
                  status:
                    type: string
                    enum:
                      - SUCCESS
                      - FLAGGED
                  flagReason:
                    type:
                      - 'null'
                      - string
                    enum:
                      - outlet_not_geolocated
                      - gps_unavailable
                      - null
                    description: Set when status=FLAGGED.
                  gpsAvailable:
                    type: boolean
                  gpsDistanceMeters:
                    type:
                      - 'null'
                      - number
                  code:
                    type:
                      - 'null'
                      - string
                    description: >-
                      Dispensed voucher code when the voucher is configured with
                      MANUAL/AUTO codes. Show this to the user.
                  redeemedAt:
                    type: string
                    format: date-time
        '401':
          description: PIN check failed.
          content:
            application/json:
              schema:
                description: PIN check failed.
                type: object
                additionalProperties: true
                properties:
                  error:
                    type: string
                    example: invalid_pin
        '403':
          description: >-
            Forbidden. Codes: `voucher_out_of_scope` (partner scope),
            `not_a_favorite` (voucher is `favoritesOnly` and user hasn't
            favorited the merchant).
          content:
            application/json:
              schema:
                description: >-
                  Forbidden. Codes: `voucher_out_of_scope` (partner scope),
                  `not_a_favorite` (voucher is `favoritesOnly` and user hasn't
                  favorited the merchant).
                type: object
                additionalProperties: true
                properties:
                  error:
                    type: string
        '404':
          description: Voucher or outlet not found.
          content:
            application/json:
              schema:
                description: Voucher or outlet not found.
                type: object
                additionalProperties: true
                properties:
                  error:
                    type: string
        '422':
          description: >-
            Cannot redeem. Codes: `voucher_inactive`, `voucher_not_yet_valid`,
            `voucher_expired`, `voucher_not_valid_at_outlet`, `outlet_inactive`,
            `outlet_pin_not_set`, `no_claim` (wallet flow without prior claim),
            `limit_reached` (per-user cycle quota), `global_cap_reached`
            (voucher's shared cycle quota — try next cycle), `too_far` (GPS
            distance exceeds radius), `codes_exhausted` (manual code pool
            empty).
          content:
            application/json:
              schema:
                description: >-
                  Cannot redeem. Codes: `voucher_inactive`,
                  `voucher_not_yet_valid`, `voucher_expired`,
                  `voucher_not_valid_at_outlet`, `outlet_inactive`,
                  `outlet_pin_not_set`, `no_claim` (wallet flow without prior
                  claim), `limit_reached` (per-user cycle quota),
                  `global_cap_reached` (voucher's shared cycle quota — try next
                  cycle), `too_far` (GPS distance exceeds radius),
                  `codes_exhausted` (manual code pool empty).
                type: object
                additionalProperties: true
                properties:
                  error:
                    type: string
                  details:
                    type: object
                    additionalProperties: true
                    description: >-
                      Context for some errors (e.g. `{ limit, used, cycleKey }`
                      for `limit_reached`).
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                description: Rate limit exceeded.
                type: object
                additionalProperties: true
                properties:
                  error:
                    type: string
                    enum:
                      - rate_limited
                  details:
                    type: object
                    properties:
                      retryAfterSeconds:
                        type: integer
        '500':
          description: Server-side failure dispensing an AUTO-generated voucher code.
          content:
            application/json:
              schema:
                description: Server-side failure dispensing an AUTO-generated voucher code.
                type: object
                additionalProperties: true
                properties:
                  error:
                    type: string
                    example: code_generation_failed
      security:
        - BearerJwt: []
components:
  securitySchemes:
    BearerJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        Bearer JWT signed by your app, with `iss` matching the issuer registered
        on the partner record. Verified against your JWKS URL or static public
        key.

````