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

# Provision a scoped integration API key for a POS terminal

> Provisions a new integration API key for a POS terminal or platform integration. The key's authority scope is determined by which of `enterprise_id`, `brand_id`, or `branch_id` are supplied (most-specific wins): a branch-scoped key can only act on one branch, a brand-scoped key on any branch under that brand, and an enterprise-scoped key on any branch under the enterprise. The raw key is formatted as `fddi_<segment>_<base64url(24 random bytes)>` and stored only as a hash, alongside a `key_prefix` (prefix plus 4 chars, used for quick-reject during verification) and `key_last_four` (for list views). The `<segment>` is currently `odo` for ALL providers regardless of POS platform (a historical artifact, not provider-derived): treat `key_prefix` as an opaque value, never parse a provider out of it, and use the `key_prefix` field returned here for matching. **The raw key is returned EXACTLY ONCE in this response and is never retrievable again**: a lost key must be regenerated, not recovered. When a `branch_id` is supplied, it is validated against the caller's own authority scope; a branch outside that scope returns `FORBIDDEN` (`403`). This is the platform-to-tenant minting handshake: a platform-scoped credential provisions per-merchant or per-branch keys, never a flat shared key. The tenant is the calling integration, and `merchant_id` is key-enforced, never free-text. Requires the `integrations` `CREATE` permission. Idempotent on the `Idempotency-Key` header: a retried create with the same key and same payload replays the original key record (including the same one-time raw key bytes) with `meta.idempotency_replayed: true`, while a same-key-different-payload request returns `IDEMPOTENCY_KEY_REUSED` (`422`).



## OpenAPI

````yaml /api-reference/openapi.yaml post /auth/keys
openapi: 3.1.0
info:
  title: Feddi Partner API
  version: '2026-06-01'
  description: >-
    Operate a Feddi closed-loop wallet and loyalty program at the point of sale:
    identify customers, read balances, accept wallet payments, run top-ups, and
    reconcile transactions.


    All amounts are integer minor units with an explicit ISO-4217 currency.
    Balances are merchant-held and closed-loop.


    Every response uses a typed envelope (`ok` / `data` / `error` / `meta`) with
    string error codes (never a bare HTTP number) and an idempotency-replay
    flag. Authenticate with the `x-api-key` header; exchange it for a
    short-lived POS terminal JWT for hot-path calls.


    Self-validate against this document (`GET /openapi`) and read `GET
    /capabilities` for the credential types, currencies, and features enabled
    for your integration before assuming any enum.


    Operations are tagged `x-feddi-availability: ga` (stable) or `beta`
    (callable, contract may still change additively); confirm what is enabled
    for your credentials via `GET /capabilities`.


    Sandbox access is granted per partner agreement; request credentials from
    your Feddi contact.
servers:
  - url: https://api.feddi.io/v1/partner
    description: production
  - url: https://api.dev.feddi.io/v1/partner
    description: dev
security:
  - ApiKeyAuth: []
tags:
  - name: platform
    description: >-
      Platform cross-cutting: health, capabilities, openapi self-serve, merchant
      provisioning, settlement, reconciliation.
  - name: checkout_session
    description: >-
      Checkout sessions: every interaction opens a session, then identity and
      basket attach to it, and payment, top-up, and offers run against it.
  - name: auth
    description: >-
      Partner authentication + onboarding: API key lifecycle, POS terminal JWT
      exchange, terminal heartbeat.
  - name: customers
    description: >-
      Customer lookup + identification: resolve identity, cashier-panel
      summaries, preferences, GDPR export/erase.
  - name: enrollment
    description: >-
      Enrollment + signup: OTP enroll, cashback claims, identity/consent,
      customer correction + merge.
  - name: payments
    description: >-
      Payments + redemption: debit wallet (promo-first), balance-check, void,
      refund, QR mint.
  - name: topup
    description: >-
      Wallet top-up: 2-step prepare/confirm, reload-bonus grants, SKU top-up,
      settlement + reconciliation.
  - name: incentives
    description: >-
      Incentives + offers: offer feeds, apply/redeem/release locks, proposals,
      budget envelopes, points, grant clawback.
  - name: transactions
    description: >-
      Transactions + receipts: transaction detail, void, receipts, disputes,
      settlements, reconciliation, exports.
  - name: webhooks
    description: >-
      Webhooks + events: subscriptions, delivery history + retry, event catalog,
      polling fallback.
paths:
  /auth/keys:
    post:
      tags:
        - auth
      summary: Provision a scoped integration API key for a POS terminal
      description: >-
        Provisions a new integration API key for a POS terminal or platform
        integration. The key's authority scope is determined by which of
        `enterprise_id`, `brand_id`, or `branch_id` are supplied (most-specific
        wins): a branch-scoped key can only act on one branch, a brand-scoped
        key on any branch under that brand, and an enterprise-scoped key on any
        branch under the enterprise. The raw key is formatted as
        `fddi_<segment>_<base64url(24 random bytes)>` and stored only as a hash,
        alongside a `key_prefix` (prefix plus 4 chars, used for quick-reject
        during verification) and `key_last_four` (for list views). The
        `<segment>` is currently `odo` for ALL providers regardless of POS
        platform (a historical artifact, not provider-derived): treat
        `key_prefix` as an opaque value, never parse a provider out of it, and
        use the `key_prefix` field returned here for matching. **The raw key is
        returned EXACTLY ONCE in this response and is never retrievable again**:
        a lost key must be regenerated, not recovered. When a `branch_id` is
        supplied, it is validated against the caller's own authority scope; a
        branch outside that scope returns `FORBIDDEN` (`403`). This is the
        platform-to-tenant minting handshake: a platform-scoped credential
        provisions per-merchant or per-branch keys, never a flat shared key. The
        tenant is the calling integration, and `merchant_id` is key-enforced,
        never free-text. Requires the `integrations` `CREATE` permission.
        Idempotent on the `Idempotency-Key` header: a retried create with the
        same key and same payload replays the original key record (including the
        same one-time raw key bytes) with `meta.idempotency_replayed: true`,
        while a same-key-different-payload request returns
        `IDEMPOTENCY_KEY_REUSED` (`422`).
      operationId: authKeyCreate
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthKeyCreateRequest'
            example:
              meta:
                partner_request_id: 9a1c4e22-7b3f-4d0a-8e21-0f9c2d3b4a5e
                occurred_at: '2026-06-05T08:00:00Z'
                sent_at: '2026-06-05T08:00:01Z'
                api_version: '2026-06-01'
              context:
                merchant_id: 11111111-1111-1111-1111-111111111111
              enterprise_id: 11111111-1111-1111-1111-111111111111
              brand_id: 44444444-4444-4444-4444-444444444444
              branch_id: 22222222-2222-2222-2222-222222222222
              label: Al-Olaya Branch POS-360-0007
      responses:
        '200':
          description: >-
            Key provisioned. `raw_key` is present ONLY in this response (or an
            idempotent replay of it) and is never returned again.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/ApiKeyCreated'
              example:
                ok: true
                data:
                  key_id: ak_01H9X2
                  raw_key: fddi_odo_3pQ8r2kL9wX1mN7vB4tY6zC0sD5fG8hJ
                  key_prefix: fddi_odo_3pQ8
                  key_last_four: 8hJ
                  scope: branch
                  enterprise_id: 11111111-1111-1111-1111-111111111111
                  brand_id: 44444444-4444-4444-4444-444444444444
                  branch_id: 22222222-2222-2222-2222-222222222222
                  label: Al-Olaya Branch POS-360-0007
                  is_sandbox: false
                  status: active
                  expires_at: null
                  created_at: '2026-06-05T08:00:01Z'
                error: null
                meta:
                  request_id: req_k1a2
                  idempotency_replayed: false
                  api_version: '2026-06-01'
        '400':
          description: >-
            `VALIDATION_ERROR` (HTTP 400), malformed body, missing required
            field, or a domain rule violated (e.g. `expires_at` in the past).
            `details` carries the field errors.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '401':
          description: '`INVALID_API_KEY` (HTTP 401), missing/invalid `x-api-key`.'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '403':
          description: >-
            `FORBIDDEN` (HTTP 403), the key is valid but lacks the
            `integrations` CREATE permission, OR the requested
            `branch_id`/`brand_id` is outside the caller's own scope (rejects
            cross-tenant provisioning).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
        '422':
          description: >-
            `IDEMPOTENCY_KEY_REUSED`, same `Idempotency-Key` header presented
            with a different payload.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ResponseEnvelope'
                  - type: object
                    properties:
                      error:
                        $ref: '#/components/schemas/Error'
      security:
        - ApiKeyAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: >-
        Partner-generated UUID; the ONE dedup key for all mutating endpoints.
        24h TTL. Same key + same payload -> byte-identical replay; same key +
        different payload -> IDEMPOTENCY_KEY_REUSED (422).
      schema:
        type: string
        format: uuid
  schemas:
    AuthKeyCreateRequest:
      type: object
      description: >-
        Provision a scoped integration API key. Scope is set by which of
        enterprise/brand/branch are supplied (most-specific wins). `merchant_id`
        in `context` is key-enforced.
      required:
        - meta
        - context
        - enterprise_id
      properties:
        meta:
          $ref: '#/components/schemas/RequestMeta'
        context:
          $ref: '#/components/schemas/PartnerContext'
        enterprise_id:
          type: string
          format: uuid
          description: >-
            The enterprise this key belongs to. Must be a tenant the caller is
            authorized for.
        brand_id:
          type:
            - string
            - 'null'
          format: uuid
          description: Optional. Narrows the key to one brand under the enterprise.
        branch_id:
          type:
            - string
            - 'null'
          format: uuid
          description: >-
            Optional. Narrows the key to one branch. Validated against the
            caller's scope; out-of-scope → 403.
        label:
          type:
            - string
            - 'null'
          maxLength: 120
          description: >-
            Human-friendly name for the ops/list UI, e.g. 'Al-Olaya Branch
            POS-360-0007'.
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
          description: Optional future expiry. Omit/null = non-expiring.
    ResponseEnvelope:
      type: object
      description: >-
        The standard response envelope that every enveloped endpoint serializes
        through. `ok` is a boolean discriminator: when `ok` is `true`, the typed
        result is carried in `data`; when `ok` is `false`, a typed error object
        is returned instead. The `meta` object is uniform across the entire API
        surface.
      required:
        - ok
        - meta
      properties:
        ok:
          type: boolean
        data:
          type:
            - object
            - 'null'
        error:
          oneOf:
            - $ref: '#/components/schemas/Error'
            - type: 'null'
        meta:
          $ref: '#/components/schemas/Meta'
    ApiKeyCreated:
      type: object
      description: >-
        The result of provisioning a key. `raw_key` appears ONLY here (or an
        idempotent replay of this exact create) and is never retrievable again.
      required:
        - key_id
        - raw_key
        - key_prefix
        - key_last_four
        - scope
        - enterprise_id
        - is_sandbox
        - status
        - created_at
      properties:
        key_id:
          type: string
          description: The API key id.
        raw_key:
          type: string
          description: >-
            The full secret key (format `fddi_odo_<base64url>`). SHOWN ONCE.
            Store it securely; a lost key must be regenerated, not recovered.
        key_prefix:
          type: string
          description: >-
            Prefix + first 4 random chars, used for quick-reject in verify and
            list display.
        key_last_four:
          type: string
          description: Last 4 chars, for list-UI disambiguation.
        scope:
          $ref: '#/components/schemas/ApiKeyScope'
        enterprise_id:
          type: string
          format: uuid
        brand_id:
          type:
            - string
            - 'null'
          format: uuid
        branch_id:
          type:
            - string
            - 'null'
          format: uuid
        label:
          type:
            - string
            - 'null'
        is_sandbox:
          type: boolean
          description: >-
            True for sandbox keys (mock providers, sandbox JWT claim, 10x rate
            limit).
        status:
          $ref: '#/components/schemas/ApiKeyStatus'
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
    Error:
      type: object
      description: >-
        The typed error object returned with every non-2xx response. `code` is a
        string enum (for example `WALLET_PROGRAM_AMBIGUOUS`,
        `CREDENTIAL_TYPE_UNSUPPORTED`, `INSUFFICIENT_FUNDS`,
        `IDEMPOTENCY_KEY_REUSED`, `CURRENCY_NOT_SUPPORTED`), **never a bare HTTP
        status number**. Inspect `code` for programmatic branching, not the HTTP
        status. Some codes echo the valid set in `details` so clients can
        present or reconcile the accepted values: for example
        `CREDENTIAL_TYPE_UNSUPPORTED` lists the supported credential types,
        `CURRENCY_NOT_SUPPORTED` lists the supported ISO-4217 currencies, and
        `WALLET_PROGRAM_AMBIGUOUS` lists the candidate `wallet_program_id`
        values that matched the request.
      required:
        - code
        - message
      properties:
        code:
          type: string
          enum:
            - INSUFFICIENT_FUNDS
            - INVALID_API_KEY
            - CREDENTIAL_TYPE_UNSUPPORTED
            - CURRENCY_NOT_SUPPORTED
            - IDEMPOTENCY_KEY_REUSED
            - WALLET_PROGRAM_AMBIGUOUS
            - REQUIRES_DYNAMIC_CREDENTIAL
            - NOT_FOUND
            - FORBIDDEN
            - VALIDATION_ERROR
            - RATE_LIMITED
            - CONFLICT
            - INTERNAL_SERVER_ERROR
        message:
          type: string
        details:
          type: object
          additionalProperties: true
    RequestMeta:
      type: object
      description: >-
        Common request metadata shared across all Partner API domains.
        `partner_request_id` is a correlation identifier only: it appears in
        logs and responses for tracing but is **never used as the deduplication
        key**. Request deduplication is keyed exclusively on the
        `Idempotency-Key` header.
      required:
        - partner_request_id
        - api_version
      properties:
        partner_request_id:
          type: string
          format: uuid
          description: Partner's own correlation id. NOT the idempotency basis.
        occurred_at:
          type: string
          format: date-time
          description: >-
            When the event happened at the POS (RFC3339). Clock-drift signal vs
            sent_at.
        sent_at:
          type: string
          format: date-time
          description: When the partner sent the request (RFC3339).
        api_version:
          type: string
          description: The single API version field.
          example: '2026-06-01'
    PartnerContext:
      type: object
      description: >-
        The canonical `context` block. `merchant_id` is key-enforced to a
        merchant the caller is authorized for, never a free-text trust field.
        `cashier_id` is server-trusted only when it is signed into the terminal
        JWT.
      required:
        - merchant_id
      properties:
        merchant_id:
          type: string
          format: uuid
          description: The resolved Feddi tenant. Key-enforced.
        branch_id:
          type:
            - string
            - 'null'
          format: uuid
        terminal_id:
          type:
            - string
            - 'null'
        cashier_id:
          type:
            - string
            - 'null'
          description: Server-trusted only when present in the signed terminal JWT.
        partner_session_id:
          type:
            - string
            - 'null'
          description: The partner's own order/session ref.
        feddi_session_id:
          type:
            - string
            - 'null'
          format: uuid
          description: The checkout_session id, when this call hangs off a session.
    Meta:
      type: object
      description: >-
        The uniform response metadata block returned on every response.
        `data_completeness_score` is computed per call and reports the
        completeness of the returned data. `decision_trace_id` is present on
        responses that carry decision or insight output and can be used to
        correlate the response with its reasoning. `capabilities` is an
        additive, response-level array of hints advertising features the caller
        may use, and may be extended over time without notice.
      required:
        - request_id
        - api_version
      properties:
        request_id:
          type: string
          description: Feddi-issued correlation id for this response.
        idempotency_replayed:
          type: boolean
          description: True when this response was replayed from the idempotency store.
        api_version:
          type: string
          description: The single API version field.
          example: '2026-06-01'
        data_completeness_score:
          type: integer
          minimum: 0
          maximum: 100
          description: >-
            0-100, computed per call (basket/identity/tax/category/consent
            presence).
        decision_trace_id:
          type:
            - string
            - 'null'
          description: Opaque trace id on intelligence-bearing responses; one per response.
        capabilities:
          type: object
          additionalProperties: true
          description: Additive response-level capability hints.
    ApiKeyScope:
      type: string
      enum:
        - enterprise
        - brand
        - branch
      description: >-
        The authority scope of a key, derived from which of
        enterprise/brand/branch ids are set (most-specific present wins).
    ApiKeyStatus:
      type: string
      enum:
        - active
        - inactive
      description: >-
        Key lifecycle status. `inactive` = revoked (soft); `active` and
        `inactive` mirror the key's enabled state.
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Partner API key (platform- or merchant-scoped). Contract key-enforces
        context.merchant_id.

````