{
  "openapi": "3.1.0",
  "info": {
    "title": "Earlybirdo App Redemption API",
    "version": "1.0.0-beta",
    "description": "Server-to-server app activation. Never embed keys in a client. Persist the exact operation before redemption, provision access once per receipt, and reconcile status. No customer PII is returned. Activation does not mark delivery complete."
  },
  "servers": [
    {
      "url": "https://{project}.supabase.co/functions/v1/redemption-api/v1",
      "variables": {
        "project": {
          "default": "YOUR_PROJECT"
        }
      }
    }
  ],
  "security": [
    {
      "serverKey": []
    }
  ],
  "paths": {
    "/redemptions/preview": {
      "post": {
        "operationId": "previewRedemption",
        "summary": "Check a code without consuming it",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PreviewRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Eligible for activation; no account binding yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PreviewResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request, invalid code, body over 4096 bytes or query parameters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, expired or revoked server key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Browser Origin header is not accepted; call from your backend",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown route or code/receipt not in this key\u2019s campaign",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not eligible, already redeemed, or idempotency payload conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Campaign limit: 120 requests/minute across all keys and endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until retry; currently 60"
              }
            }
          },
          "503": {
            "description": "Temporary failure; retry the same operation with backoff",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/redemptions": {
      "post": {
        "operationId": "redeemCode",
        "summary": "Bind the code once to an authenticated app account",
        "description": "Derive external_user_id from your own verified session. Reuse the exact idempotency key and body after timeout/429/503. Replay returns current status; never grant access if it is no longer active. The key is scoped to the campaign and remains valid for replay after key rotation.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 128,
              "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{7,127}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RedeemRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Activated; replayed=false",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RedemptionResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            }
          },
          "200": {
            "description": "Matching retry; replayed=true",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RedemptionResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request, invalid code, body over 4096 bytes or query parameters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, expired or revoked server key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Browser Origin header is not accepted; call from your backend",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown route or code/receipt not in this key\u2019s campaign",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not eligible, already redeemed, or idempotency payload conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Campaign limit: 120 requests/minute across all keys and endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until retry; currently 60"
              }
            }
          },
          "503": {
            "description": "Temporary failure; retry the same operation with backoff",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/redemptions/{id}": {
      "get": {
        "operationId": "getRedemption",
        "summary": "Read current purchase eligibility for a saved receipt",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current purchase state; this does not encode the access term",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request, invalid code, body over 4096 bytes or query parameters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, expired or revoked server key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Browser Origin header is not accepted; call from your backend",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown route or code/receipt not in this key\u2019s campaign",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not eligible, already redeemed, or idempotency payload conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Campaign limit: 120 requests/minute across all keys and endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until retry; currently 60"
              }
            }
          },
          "503": {
            "description": "Temporary failure; retry the same operation with backoff",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "serverKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ebr_ followed by 64 lowercase hexadecimal characters",
        "description": "Campaign-scoped secret, shown once and expires after 90 days. Supabase JWTs and MCP tokens are not accepted."
      }
    },
    "schemas": {
      "PreviewRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "code"
        ],
        "properties": {
          "code": {
            "type": "string",
            "maxLength": 64,
            "description": "16 hexadecimal characters after stripping spaces and hyphens. Send a code, not a QR URL.",
            "example": "ABCD-1234-ABCD-5678"
          }
        }
      },
      "RedeemRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "code",
          "external_user_id"
        ],
        "properties": {
          "code": {
            "type": "string",
            "maxLength": 64,
            "description": "16 hexadecimal characters after stripping spaces and hyphens. Send a code, not a QR URL.",
            "example": "ABCD-1234-ABCD-5678"
          },
          "external_user_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$",
            "description": "Opaque ID from your verified customer session; do not send an email address."
          }
        }
      },
      "Receipt": {
        "type": "object",
        "required": [
          "id",
          "campaign_id",
          "order_id",
          "perk_id",
          "external_user_id",
          "activated_at",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "campaign_id": {
            "type": "string",
            "format": "uuid"
          },
          "order_id": {
            "type": "string",
            "format": "uuid"
          },
          "perk_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "external_user_id": {
            "type": "string"
          },
          "activated_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "suspended",
              "revoked"
            ],
            "description": "Purchase eligibility only; the developer enforces the purchased access term."
          }
        }
      },
      "PreviewResponse": {
        "type": "object",
        "required": [
          "data",
          "request_id"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "format": "uuid"
          },
          "data": {
            "type": "object",
            "required": [
              "campaign_id",
              "perk_id",
              "campaign_title",
              "perk_name",
              "eligible"
            ],
            "properties": {
              "campaign_id": {
                "type": "string",
                "format": "uuid"
              },
              "perk_id": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uuid"
              },
              "campaign_title": {
                "type": "string"
              },
              "perk_name": {
                "type": "string"
              },
              "eligible": {
                "type": "boolean",
                "const": true
              }
            }
          }
        }
      },
      "StatusResponse": {
        "type": "object",
        "required": [
          "data",
          "request_id"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Receipt"
          },
          "request_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "RedemptionResponse": {
        "type": "object",
        "required": [
          "data",
          "replayed",
          "request_id"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Receipt"
          },
          "replayed": {
            "type": "boolean"
          },
          "request_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "request_id"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "format": "uuid"
          },
          "error": {
            "type": "object",
            "required": [
              "code"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_key",
                  "server_only",
                  "invalid_request",
                  "invalid_code",
                  "not_found",
                  "method_not_allowed",
                  "not_eligible",
                  "already_redeemed",
                  "idempotency_conflict",
                  "rate_limited",
                  "temporarily_unavailable"
                ]
              }
            }
          }
        }
      }
    }
  }
}
