{
  "openapi": "3.1.2",
  "info": {
    "title": "Acme Loyalty API",
    "description": "Loyalty program for Acme Webshop customers. Members earn points on every order and redeem them for rewards in the shop.",
    "contact": {
      "email": "loyalty-api@acmewebshop.com"
    },
    "license": {
      "name": "Proprietary"
    },
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.acmewebshop.com/loyalty/v1"
    }
  ],
  "paths": {
    "/members": {
      "get": {
        "tags": [
          "Members"
        ],
        "summary": "List members",
        "description": "Returns enrolled members, newest first. Filter by email to look up a single customer.",
        "operationId": "listMembers",
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "description": "Filter by exact email address",
            "schema": {
              "type": "string",
              "format": "email"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of members",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Maximum number of requests allowed in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1000
              },
              "X-RateLimit-Remaining": {
                "description": "Requests remaining in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 987
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp (seconds) at which the current window resets.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1787479200
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-routebase-folder-path": "Members"
      },
      "post": {
        "tags": [
          "Members"
        ],
        "summary": "Enroll a customer in the loyalty program",
        "description": "Creates a member for an existing webshop customer. New members start in the BRONZE tier with zero points.",
        "operationId": "enrollMember",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewMember"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Member enrolled",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Maximum number of requests allowed in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1000
              },
              "X-RateLimit-Remaining": {
                "description": "Requests remaining in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 987
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp (seconds) at which the current window resets.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1787479200
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-routebase-folder-path": "Members"
      }
    },
    "/members/{memberId}": {
      "get": {
        "tags": [
          "Members"
        ],
        "summary": "Get a member",
        "description": "Returns one member with their tier and current point balance.",
        "operationId": "getMember",
        "parameters": [
          {
            "$ref": "#/components/parameters/MemberId"
          }
        ],
        "responses": {
          "200": {
            "description": "The member",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Maximum number of requests allowed in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1000
              },
              "X-RateLimit-Remaining": {
                "description": "Requests remaining in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 987
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp (seconds) at which the current window resets.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1787479200
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-routebase-folder-path": "Members"
      }
    },
    "/members/{memberId}/points": {
      "get": {
        "tags": [
          "Points"
        ],
        "summary": "Get a member\u0027s point balance",
        "description": "Returns the current balance together with the most recent transactions.",
        "operationId": "getPointsBalance",
        "parameters": [
          {
            "$ref": "#/components/parameters/MemberId"
          }
        ],
        "responses": {
          "200": {
            "description": "Current balance and the most recent transactions",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Maximum number of requests allowed in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1000
              },
              "X-RateLimit-Remaining": {
                "description": "Requests remaining in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 987
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp (seconds) at which the current window resets.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1787479200
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PointsBalance"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-routebase-folder-path": "Points"
      },
      "post": {
        "tags": [
          "Points"
        ],
        "summary": "Credit points for an order",
        "description": "Credits one point per euro of the order total. Send the same Idempotency-Key when you retry, so an order is never credited twice.",
        "operationId": "earnPoints",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/MemberId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EarnPointsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Points credited",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Maximum number of requests allowed in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1000
              },
              "X-RateLimit-Remaining": {
                "description": "Requests remaining in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 987
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp (seconds) at which the current window resets.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1787479200
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PointsTransaction"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-routebase-folder-path": "Points"
      }
    },
    "/members/{memberId}/redemptions": {
      "post": {
        "tags": [
          "Rewards"
        ],
        "summary": "Redeem points for a reward",
        "description": "Deducts the reward\u0027s point cost from the member\u0027s balance. Fails with 409 when the balance is too low.",
        "operationId": "redeemReward",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/MemberId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RedemptionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Reward redeemed",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Maximum number of requests allowed in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1000
              },
              "X-RateLimit-Remaining": {
                "description": "Requests remaining in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 987
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp (seconds) at which the current window resets.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1787479200
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Redemption"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Not enough points for this reward",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Maximum number of requests allowed in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1000
              },
              "X-RateLimit-Remaining": {
                "description": "Requests remaining in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 987
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp (seconds) at which the current window resets.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1787479200
              }
            },
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-routebase-folder-path": "Rewards"
      }
    },
    "/rewards": {
      "get": {
        "tags": [
          "Rewards"
        ],
        "summary": "List available rewards",
        "description": "Returns the rewards members can currently redeem, cheapest first.",
        "operationId": "listRewards",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "The reward catalog",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Maximum number of requests allowed in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1000
              },
              "X-RateLimit-Remaining": {
                "description": "Requests remaining in the current window.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 987
              },
              "X-RateLimit-Reset": {
                "description": "Unix timestamp (seconds) at which the current window resets.",
                "required": true,
                "schema": {
                  "type": "integer"
                },
                "example": 1787479200
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RewardPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "x-routebase-folder-path": "Rewards"
      }
    }
  },
  "components": {
    "schemas": {
      "Member": {
        "required": [
          "id",
          "email",
          "name",
          "tier",
          "pointsBalance",
          "enrolledAt"
        ],
        "type": "object",
        "properties": {
          "id": {
            "examples": [
              "mem_8Kq2vN"
            ],
            "type": "string",
            "description": "Public ID of the member"
          },
          "email": {
            "examples": [
              "maria.lopez@example.com"
            ],
            "type": "string",
            "description": "Email address of the webshop account",
            "format": "email"
          },
          "name": {
            "examples": [
              "Maria Lopez"
            ],
            "type": "string",
            "description": "Display name"
          },
          "tier": {
            "examples": [
              "SILVER"
            ],
            "enum": [
              "BRONZE",
              "SILVER",
              "GOLD"
            ],
            "type": "string",
            "description": "Loyalty tier, raised by points earned in the last 12 months"
          },
          "pointsBalance": {
            "examples": [
              1250
            ],
            "type": "integer",
            "description": "Points available to redeem"
          },
          "enrolledAt": {
            "examples": [
              "2026-03-14T09:30:00Z"
            ],
            "type": "string",
            "description": "When the customer joined the program",
            "format": "date-time"
          }
        },
        "description": "A webshop customer enrolled in the loyalty program"
      },
      "MemberPage": {
        "required": [
          "items",
          "total"
        ],
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Member"
            },
            "description": "Members on this page"
          },
          "total": {
            "examples": [
              1
            ],
            "type": "integer",
            "description": "Number of members across all pages"
          }
        },
        "description": "One page of members"
      },
      "NewMember": {
        "required": [
          "email",
          "name"
        ],
        "type": "object",
        "properties": {
          "email": {
            "examples": [
              "maria.lopez@example.com"
            ],
            "type": "string",
            "description": "Email address of the webshop account",
            "format": "email"
          },
          "name": {
            "examples": [
              "Maria Lopez"
            ],
            "type": "string",
            "description": "Display name"
          }
        },
        "description": "Data needed to enroll a customer"
      },
      "PointsBalance": {
        "required": [
          "memberId",
          "balance",
          "recentTransactions"
        ],
        "type": "object",
        "properties": {
          "memberId": {
            "examples": [
              "mem_8Kq2vN"
            ],
            "type": "string",
            "description": "Public ID of the member"
          },
          "balance": {
            "examples": [
              1250
            ],
            "type": "integer",
            "description": "Points available to redeem"
          },
          "recentTransactions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PointsTransaction"
            },
            "description": "The latest transactions, newest first"
          }
        },
        "description": "A member\u0027s balance with the latest transactions"
      },
      "EarnPointsRequest": {
        "required": [
          "orderId",
          "orderTotal"
        ],
        "type": "object",
        "properties": {
          "orderId": {
            "examples": [
              "ord_1042"
            ],
            "type": "string",
            "description": "The webshop order"
          },
          "orderTotal": {
            "examples": [
              120.5
            ],
            "type": "number",
            "description": "Order total in EUR, one point per euro"
          }
        },
        "description": "The order to credit points for"
      },
      "PointsTransaction": {
        "required": [
          "id",
          "type",
          "points",
          "createdAt"
        ],
        "type": "object",
        "properties": {
          "id": {
            "examples": [
              "txn_4Rt7pL"
            ],
            "type": "string",
            "description": "Public ID of the transaction"
          },
          "type": {
            "examples": [
              "EARN"
            ],
            "enum": [
              "EARN",
              "REDEEM"
            ],
            "type": "string",
            "description": "Whether points were earned or redeemed"
          },
          "points": {
            "examples": [
              120
            ],
            "type": "integer",
            "description": "Number of points moved"
          },
          "orderId": {
            "examples": [
              "ord_1042"
            ],
            "type": "string",
            "description": "The webshop order that earned the points"
          },
          "createdAt": {
            "examples": [
              "2026-09-20T14:05:00Z"
            ],
            "type": "string",
            "description": "When the transaction was booked",
            "format": "date-time"
          }
        },
        "description": "A single credit or debit of points"
      },
      "Reward": {
        "required": [
          "id",
          "name",
          "pointsCost"
        ],
        "type": "object",
        "properties": {
          "id": {
            "examples": [
              "rwd_pruner"
            ],
            "type": "string",
            "description": "Public ID of the reward"
          },
          "name": {
            "examples": [
              "Bypass pruning shears"
            ],
            "type": "string",
            "description": "Product name shown in the shop"
          },
          "pointsCost": {
            "examples": [
              900
            ],
            "type": "integer",
            "description": "Points needed to redeem"
          }
        },
        "description": "An item members can redeem with points"
      },
      "RewardPage": {
        "required": [
          "items",
          "total"
        ],
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Reward"
            },
            "description": "Rewards on this page"
          },
          "total": {
            "examples": [
              12
            ],
            "type": "integer",
            "description": "Number of rewards across all pages"
          }
        },
        "description": "One page of the reward catalog"
      },
      "Redemption": {
        "required": [
          "id",
          "rewardId",
          "pointsSpent",
          "remainingBalance"
        ],
        "type": "object",
        "properties": {
          "id": {
            "examples": [
              "red_9Ws3xD"
            ],
            "type": "string",
            "description": "Public ID of the redemption"
          },
          "rewardId": {
            "examples": [
              "rwd_pruner"
            ],
            "type": "string",
            "description": "Public ID of the redeemed reward"
          },
          "pointsSpent": {
            "examples": [
              900
            ],
            "type": "integer",
            "description": "Points deducted from the balance"
          },
          "remainingBalance": {
            "examples": [
              350
            ],
            "type": "integer",
            "description": "Balance after the redemption"
          }
        },
        "description": "A completed redemption"
      },
      "RedemptionRequest": {
        "required": [
          "rewardId"
        ],
        "type": "object",
        "properties": {
          "rewardId": {
            "examples": [
              "rwd_pruner"
            ],
            "type": "string",
            "description": "Public ID of the reward"
          }
        },
        "description": "The reward to redeem"
      },
      "Problem": {
        "required": [
          "type",
          "title",
          "status"
        ],
        "type": "object",
        "properties": {
          "type": {
            "examples": [
              "https://api.acmewebshop.com/problems/member-not-found"
            ],
            "type": "string",
            "description": "URI that identifies the problem type",
            "format": "uri"
          },
          "title": {
            "examples": [
              "Member not found"
            ],
            "type": "string",
            "description": "Short, human-readable summary"
          },
          "status": {
            "examples": [
              404
            ],
            "type": "integer",
            "description": "HTTP status code"
          },
          "detail": {
            "examples": [
              "Member mem_8Kq2vN does not exist"
            ],
            "type": "string",
            "description": "Explanation specific to this occurrence"
          }
        },
        "description": "Error details in the RFC 9457 problem format"
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request body is invalid",
        "content": {
          "application/problem\u002Bjson": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid bearer token",
        "content": {
          "application/problem\u002Bjson": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "The member does not exist",
        "content": {
          "application/problem\u002Bjson": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "parameters": {
      "MemberId": {
        "name": "MemberId",
        "in": "path",
        "description": "Public ID of the member",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "Limit": {
        "name": "Limit",
        "in": "query",
        "description": "Maximum number of items to return",
        "schema": {
          "type": "integer"
        }
      },
      "Offset": {
        "name": "Offset",
        "in": "query",
        "description": "Number of items to skip",
        "schema": {
          "type": "integer"
        }
      },
      "IdempotencyKey": {
        "name": "IdempotencyKey",
        "in": "header",
        "description": "Unique key per operation, so a retried request is applied only once",
        "schema": {
          "type": "string"
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "description": "Access token issued by the Acme identity service",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  },
  "tags": [
    {
      "name": "Members",
      "description": "Loyalty program members"
    },
    {
      "name": "Points",
      "description": "Point balances and transactions"
    },
    {
      "name": "Rewards",
      "description": "Reward catalog and redemptions"
    }
  ],
  "x-routebase-folders": [
    {
      "name": "Members",
      "children": []
    },
    {
      "name": "Points",
      "children": []
    },
    {
      "name": "Rewards",
      "children": []
    }
  ]
}