{
  "openapi": "3.1.0",
  "info": {
    "title": "Loyumi Public API",
    "version": "1.2.0",
    "description": "Environment-scoped APIs for purchase earning, returns and bounded clawbacks, registered engagement events, cart-offer evaluation, milestone choice benefits, and member balances. Monetary amounts are integers in minor currency units. Point amounts are integers."
  },
  "servers": [
    {
      "url": "https://app.loyumi.com/api/v1",
      "description": "Loyumi API; the key and X-Environment header select Sandbox or production"
    }
  ],
  "tags": [
    { "name": "Commerce", "description": "Earn, return, and offer-evaluation operations" },
    { "name": "Engagement", "description": "Registered custom events and milestone benefits" },
    { "name": "Members", "description": "Member and balance reads" }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Environment-specific server credential. Never embed it in browser JavaScript or a mobile binary."
      }
    },
    "parameters": {
      "environment": {
        "name": "X-Environment",
        "in": "header",
        "required": true,
        "description": "Environment ID that owns the bearer credential.",
        "schema": { "type": "string", "minLength": 1 }
      },
      "idempotency": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Stable identity for one business mutation. Reuse the same key and identical body when retrying.",
        "schema": { "type": "string", "minLength": 1 }
      },
      "requestId": {
        "name": "X-Request-Id",
        "in": "header",
        "required": false,
        "description": "Caller-provided trace identifier. Loyumi creates one when omitted.",
        "schema": { "type": "string" }
      }
    },
    "schemas": {
      "CartItem": {
        "type": "object",
        "required": ["sku", "quantity", "unitPriceMinor"],
        "properties": {
          "sku": { "type": "string", "minLength": 1 },
          "category": { "type": "string" },
          "quantity": { "type": "integer", "minimum": 1 },
          "unitPriceMinor": { "type": "integer", "minimum": 1, "description": "Price for one unit in integer minor currency units." }
        }
      },
      "ApiError": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" },
              "requestId": { "type": "string" }
            }
          }
        }
      },
      "PurchaseRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["type", "programId", "externalCustomerId", "amountMinor", "channel", "sourceReference"],
        "properties": {
          "type": { "const": "purchase.completed" },
          "programId": { "type": "string", "minLength": 1 },
          "externalCustomerId": { "type": "string", "minLength": 1 },
          "amountMinor": { "type": "integer", "minimum": 1, "description": "Completed order value in integer minor currency units." },
          "channel": { "type": "string", "enum": ["web", "store", "app"] },
          "sourceReference": { "type": "string", "minLength": 1, "description": "Stable order reference from the commerce system." },
          "couponCode": { "type": "string", "description": "Optional coupon code associated with this purchase." },
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/CartItem" } }
        }
      },
      "PurchaseOutcome": {
        "type": "object",
        "required": ["id", "status", "requestId", "member", "award", "campaigns"],
        "properties": {
          "id": { "type": "string" },
          "status": { "const": "committed" },
          "requestId": { "type": "string" },
          "member": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "externalCustomerId": { "type": "string" },
              "tier": { "type": "string" }
            }
          },
          "award": {
            "type": "object",
            "properties": {
              "points": { "type": "integer" },
              "basePoints": { "type": "integer" },
              "campaignPoints": { "type": "integer" }
            }
          },
          "campaigns": { "type": "array", "items": { "type": "object" } },
          "referral": { "type": ["object", "null"] },
          "coupon": { "type": ["string", "null"] }
        }
      },
      "IdempotentOutcome": {
        "type": "object",
        "required": ["id", "idempotent", "requestId"],
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string" },
          "state": { "type": "string" },
          "idempotent": { "const": true },
          "requestId": { "type": "string" }
        }
      },
      "ReturnRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["sourceReference", "returnReference", "returnAmountMinor"],
        "properties": {
          "sourceReference": { "type": "string", "minLength": 1, "description": "Reference of the original completed order." },
          "returnReference": { "type": "string", "minLength": 1, "description": "Stable identity for this full or partial return." },
          "returnAmountMinor": { "type": "integer", "minimum": 1, "description": "Amount being returned in integer minor currency units." }
        }
      },
      "CustomEventRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["type", "externalCustomerId", "sourceReference"],
        "properties": {
          "type": { "type": "string", "minLength": 1, "description": "Name of an active event definition in the selected environment." },
          "externalCustomerId": { "type": "string", "minLength": 1 },
          "sourceReference": { "type": "string", "minLength": 1 },
          "occurredAt": { "type": "string", "format": "date-time" },
          "attributes": { "type": "object", "additionalProperties": true }
        }
      },
      "OfferEvaluationRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["items"],
        "properties": {
          "programId": { "type": "string", "description": "Required to include program-scoped offers." },
          "couponCode": { "type": "string" },
          "items": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/CartItem" } }
        }
      },
      "ChoiceBenefitRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["externalCustomerId", "choiceSetId", "rewardId", "sourceReference"],
        "properties": {
          "externalCustomerId": { "type": "string", "minLength": 1 },
          "choiceSetId": { "type": "string", "minLength": 1 },
          "rewardId": { "type": "string", "minLength": 1 },
          "sourceReference": { "type": "string", "minLength": 1 }
        }
      }
    }
  },
  "security": [{ "bearerAuth": [] }],
  "x-rate-limit": { "requests": 600, "window": "60 seconds", "retryHeader": "Retry-After" },
  "paths": {
    "/events": {
      "post": {
        "tags": ["Commerce"],
        "operationId": "postPurchase",
        "summary": "Post a completed purchase and award points",
        "description": "Evaluates the active earn rule, tier multiplier, audiences, campaign caps, pending policy, expiration policy, and member limit in one idempotent transaction. Requires events:write.",
        "parameters": [
          { "$ref": "#/components/parameters/environment" },
          { "$ref": "#/components/parameters/idempotency" },
          { "$ref": "#/components/parameters/requestId" }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PurchaseRequest" } } }
        },
        "responses": {
          "200": { "description": "Existing transaction returned for a repeated idempotency key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IdempotentOutcome" } } } },
          "201": { "description": "New award committed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PurchaseOutcome" } } } },
          "400": { "description": "Invalid JSON or missing Idempotency-Key." },
          "401": { "description": "Credential or X-Environment is absent, invalid, expired, revoked, or crossed between environments." },
          "403": { "description": "Credential lacks events:write." },
          "404": { "description": "Program or active member was not found in the selected environment." },
          "409": { "description": "Production is locked, or a governed policy, cap, or state conflict blocked the award." },
          "413": { "description": "JSON body exceeds 128 KiB." },
          "415": { "description": "Content-Type must be application/json." },
          "422": { "description": "Event type or required field is invalid." },
          "429": { "description": "Credential exceeded 600 requests in one minute. Honor Retry-After." },
          "500": { "description": "Internal processing error. Retry safely with the same key and body." }
        }
      }
    },
    "/returns": {
      "post": {
        "tags": ["Commerce"],
        "operationId": "postReturn",
        "summary": "Return all or part of an order and claw back points",
        "description": "Uses the original source reference to derive the related order and program. Calculates a bounded proportional clawback from the order's remaining active and pending point lots. Requires events:write.",
        "parameters": [
          { "$ref": "#/components/parameters/environment" },
          { "$ref": "#/components/parameters/idempotency" },
          { "$ref": "#/components/parameters/requestId" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReturnRequest" } } } },
        "responses": {
          "200": { "description": "Existing clawback transaction returned for a repeated idempotency key." },
          "201": { "description": "Return and proportional clawback committed." },
          "400": { "description": "Invalid JSON or missing Idempotency-Key." },
          "401": { "description": "Invalid credential or environment." },
          "403": { "description": "Credential lacks events:write." },
          "404": { "description": "Original order was not found." },
          "409": { "description": "Production is locked, return exceeds remaining order value, or original points cannot be clawed back." },
          "413": { "description": "JSON body exceeds 128 KiB." },
          "415": { "description": "Content-Type must be application/json." },
          "422": { "description": "sourceReference, returnReference, or positive returnAmountMinor is invalid." },
          "429": { "description": "Rate limit exceeded. Honor Retry-After." },
          "500": { "description": "Internal processing error." }
        }
      }
    },
    "/custom-events": {
      "post": {
        "tags": ["Engagement"],
        "operationId": "postCustomEvent",
        "summary": "Post a registered engagement event",
        "description": "Records a governed event and advances matching challenges atomically, returning any awards created when a target completes. Requires events:write.",
        "parameters": [
          { "$ref": "#/components/parameters/environment" },
          { "$ref": "#/components/parameters/idempotency" },
          { "$ref": "#/components/parameters/requestId" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CustomEventRequest" } } } },
        "responses": {
          "200": { "description": "Existing event returned for a repeated source reference." },
          "201": { "description": "Event accepted and challenge awards returned." },
          "400": { "description": "Invalid JSON." },
          "401": { "description": "Invalid credential or environment." },
          "403": { "description": "Credential lacks events:write." },
          "404": { "description": "Active event definition or member was not found." },
          "413": { "description": "JSON body exceeds 128 KiB." },
          "415": { "description": "Content-Type must be application/json." },
          "422": { "description": "Idempotency-Key or a required event field is missing." },
          "429": { "description": "Rate limit exceeded. Honor Retry-After." },
          "500": { "description": "Internal processing error." }
        }
      }
    },
    "/offers/evaluate": {
      "post": {
        "tags": ["Commerce"],
        "operationId": "evaluateOffers",
        "summary": "Evaluate active cart offers",
        "description": "Returns a deterministic discount plan under exclusive, stack, and best-value policies without mutating the cart or consuming a coupon. Requires events:write.",
        "parameters": [
          { "$ref": "#/components/parameters/environment" },
          { "$ref": "#/components/parameters/requestId" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OfferEvaluationRequest" } } } },
        "responses": {
          "200": { "description": "Applicable effects, coupon validity, subtotal, discount, and total returned." },
          "400": { "description": "Invalid JSON." },
          "401": { "description": "Invalid credential or environment." },
          "403": { "description": "Credential lacks events:write." },
          "413": { "description": "JSON body exceeds 128 KiB." },
          "415": { "description": "Content-Type must be application/json." },
          "422": { "description": "Cart is empty or contains an item without a SKU or positive unitPriceMinor." },
          "429": { "description": "Rate limit exceeded. Honor Retry-After." },
          "500": { "description": "Internal processing error." }
        }
      }
    },
    "/choice-benefits": {
      "post": {
        "tags": ["Engagement"],
        "operationId": "claimChoiceBenefit",
        "summary": "Claim a milestone choice benefit",
        "description": "Fulfills one eligible reward choice without spending points and decrements the member's remaining selections. Requires events:write.",
        "parameters": [
          { "$ref": "#/components/parameters/environment" },
          { "$ref": "#/components/parameters/idempotency" },
          { "$ref": "#/components/parameters/requestId" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChoiceBenefitRequest" } } } },
        "responses": {
          "200": { "description": "Existing fulfillment returned for a repeated idempotency key." },
          "201": { "description": "Choice fulfilled and remaining selections returned." },
          "400": { "description": "Invalid JSON." },
          "401": { "description": "Invalid credential or environment." },
          "403": { "description": "Credential lacks events:write." },
          "404": { "description": "Member or active choice set was unavailable." },
          "409": { "description": "No eligible choice or selected reward inventory remains." },
          "413": { "description": "JSON body exceeds 128 KiB." },
          "415": { "description": "Content-Type must be application/json." },
          "422": { "description": "Idempotency-Key or a required choice field is missing." },
          "429": { "description": "Rate limit exceeded. Honor Retry-After." },
          "500": { "description": "Internal processing error." }
        }
      }
    },
    "/members/{externalId}": {
      "get": {
        "tags": ["Members"],
        "operationId": "getMember",
        "summary": "Get a member, balances, tier, and qualification state",
        "description": "Returns the member identity state and consent status plus every program profile and its available, pending, and reserved account balances. Requires members:read.",
        "parameters": [
          { "$ref": "#/components/parameters/environment" },
          { "$ref": "#/components/parameters/requestId" },
          { "name": "externalId", "in": "path", "required": true, "description": "URL-encoded external customer ID.", "schema": { "type": "string", "minLength": 1 } }
        ],
        "responses": {
          "200": { "description": "Member, consent state, program profiles, tiers, qualification totals, and account balances returned with Cache-Control: no-store." },
          "401": { "description": "Invalid credential or environment." },
          "403": { "description": "Credential lacks members:read." },
          "404": { "description": "Member was not found in the selected environment." },
          "429": { "description": "Rate limit exceeded. Honor Retry-After." },
          "500": { "description": "Internal processing error." }
        }
      }
    }
  }
}
