openapi: 3.1.0
info:
  title: e-Próspera API
  version: "1.0.0"
  description: |
    Public API for Próspera's resident registry, legal-entity incorporation, OAuth identity,
    and agent-delegated workflows.

    Hand-maintained alongside the rendered docs at
    https://portal.eprospera.com/api-docs/. Update this file whenever an endpoint
    response shape, scope requirement, or error contract changes.
  contact:
    url: https://portal.eprospera.com/api-docs/

servers:
  - url: https://portal.eprospera.com
    description: Production
  - url: https://staging-portal.eprospera.com
    description: Staging

tags:
  - name: Registry
    description: Look up residents and legal entities.
  - name: Legal Entities
    description: Read legal-entity records and documents.
  - name: Applications
    description: Create, pay, and track legal-entity applications.
  - name: Referrals
    description: Catalyst Program referral attribution.
  - name: Me
    description: Endpoints scoped to the authenticated user. Accept OAuth tokens; some also accept Agent Keys.
  - name: OAuth
    description: OAuth 2.0 / OpenID Connect endpoints.

security:
  - bearerApiKey: []

paths:
  /api/v1/verify_rpn:
    post:
      tags: [Registry]
      summary: Verify whether an RPN exists and is active
      description: |
        Rate-limited to 5,000 requests / 24 h and 50 / minute per API key.
      security:
        - bearerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rpn]
              properties:
                rpn:
                  type: string
                  pattern: "^[89][0-9]{13}$"
                  example: "80000000000012"
      responses:
        "200":
          description: Lookup result
          content:
            application/json:
              schema:
                type: object
                required: [result, active]
                properties:
                  result:
                    type: string
                    enum: [found_legal_entity, found_natural_person, not_found]
                  active:
                    type: boolean
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/registries/legal_entities/search:
    post:
      tags: [Registry]
      summary: Free-text search of the legal-entity registry
      description: |
        Multi-word queries split on whitespace. Case-insensitive name match plus partial RPN match.
        Rate-limited to 5,000 / 24 h and 50 / minute per API key.
      security:
        - bearerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query:
                  type: string
                  minLength: 1
                  example: "Acme Holdings"
      responses:
        "200":
          description: Matching legal entities
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        name: { type: string }
                        extension: { type: string }
                        residentPermitNumber: { type: [string, "null"] }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/legal_entities/{id}:
    get:
      tags: [Legal Entities]
      summary: Fetch one legal entity
      description: |
        Agent Keys can only access entities whose underlying residency application
        was created via the API (`residencyApplication.createdViaAPI === true`).
      security:
        - bearerApiKey: []
      parameters:
        - $ref: "#/components/parameters/LegalEntityId"
      responses:
        "200":
          description: The legal-entity record
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LegalEntity" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/legal_entities/{id}/documents:
    get:
      tags: [Legal Entities]
      summary: List documents for a legal entity
      description: |
        Not currently paginated — full result set is returned under `data`.
        See https://portal.eprospera.com/api-docs/conventions#pagination.
      security:
        - bearerApiKey: []
      parameters:
        - $ref: "#/components/parameters/LegalEntityId"
      responses:
        "200":
          description: Documents for the entity
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Document" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/legal_entity_applications:
    get:
      tags: [Applications]
      summary: List legal-entity applications
      description: |
        Agent Keys see only API-created applications (`createdViaAPI=true`).
        Not currently paginated.
      security:
        - bearerApiKey: []
      responses:
        "200":
          description: Applications visible to the caller
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      { $ref: "#/components/schemas/LegalEntityApplication" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "500": { $ref: "#/components/responses/InternalError" }
    post:
      tags: [Applications]
      summary: Create a legal-entity application
      description: |
        Write Agent Keys require an active Manifestation of Will. With AOC pre-acceptance
        the application auto-progresses on payment; otherwise `nextSteps.signature` returns
        a browser URL the human must visit.
      security:
        - bearerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              {
                $ref: "#/components/schemas/CreateLegalEntityApplicationRequest",
              }
      responses:
        "200":
          description: Application created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LegalEntityApplication" }
                  nextSteps:
                    type: object
                    properties:
                      signature: { type: [string, "null"], format: uri }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/legal_entity_applications/{id}:
    get:
      tags: [Applications]
      summary: Fetch one application
      security:
        - bearerApiKey: []
      parameters:
        - $ref: "#/components/parameters/ApplicationId"
      responses:
        "200":
          description: Application
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LegalEntityApplication" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/legal_entity_applications/{id}/pay/coupon:
    post:
      tags: [Applications]
      summary: Apply a coupon that fully covers the invoice
      security:
        - bearerApiKey: []
      parameters:
        - $ref: "#/components/parameters/ApplicationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [couponCode]
              properties:
                couponCode: { type: string, minLength: 1 }
      responses:
        "200":
          description: Coupon applied
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: "#/components/schemas/LegalEntityApplication" }
                  message: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/legal_entity_applications/{id}/checkout_session:
    post:
      tags: [Applications]
      summary: Create a hosted-checkout session for the application invoice
      description: |
        Standard API keys only. Agent Key hosted-checkout creation is temporarily disabled;
        use the coupon-payment endpoint or have the resident pay through the portal.
      security:
        - bearerApiKey: []
      parameters:
        - $ref: "#/components/parameters/ApplicationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [paymentProvider, redirectUrl]
              properties:
                paymentProvider:
                  type: string
                  enum: [stripe, stripe-crypto, lnbits, solana-pay-ptc]
                redirectUrl: { type: string, format: uri }
                email: { type: [string, "null"], format: email }
      responses:
        "200":
          description: Checkout session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      url: { type: string, format: uri }
                  invoiceId: { type: string, format: uuid }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503":
          description: Agent Key checkout sessions are temporarily disabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/referral-codes/{code}/referrals:
    get:
      tags: [Referrals]
      summary: List referrals for a code you own
      description: Standard API keys only — Agent Keys are not supported on this route.
      security:
        - bearerApiKey: []
      parameters:
        - in: path
          name: code
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Referral attribution
          content:
            application/json:
              schema:
                type: object
                properties:
                  code: { type: string }
                  naturalPersons:
                    type: array
                    items:
                      type: object
                      properties:
                        fullName: { type: [string, "null"] }
                        referredAt: { type: string, format: date-time }
                  legalEntities:
                    type: array
                    items:
                      type: object
                      properties:
                        name: { type: string }
                        rpn: { type: string }
                        referredAt:
                          { type: [string, "null"], format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/me/natural-person:
    get:
      tags: [Me]
      summary: Personal details for the authenticated user
      description: |
        Accepts OAuth tokens (with `eprospera:person.details.read`) and Agent Keys
        (with `agent:person.details.read`). Standard API keys are NOT accepted here.
        Returns `null` if the owner has no approved residency.
      security:
        - oauth2: [eprospera:person.details.read]
        - bearerApiKey: []
      responses:
        "200":
          description: Personal details, or null
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/NaturalPerson" }
                  - { type: "null" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/me/natural-person/id-verification:
    get:
      tags: [Me]
      summary: ID-verification artifacts (signed URLs, ~1 h TTL)
      security:
        - oauth2: [eprospera:person.id_verification.read]
        - bearerApiKey: []
      responses:
        "200":
          description: ID verification record
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IdVerification" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/me/natural-person/residency:
    get:
      tags: [Me]
      summary: Residency status for the authenticated user
      security:
        - oauth2: [eprospera:person.residency.read]
        - bearerApiKey: []
      responses:
        "200":
          description: Residency status
          content:
            application/json:
              schema:
                type: object
                properties:
                  wasEverResident: { type: boolean }
                  activeResidency:
                    oneOf:
                      - type: object
                        properties:
                          effectiveDate: { type: string, format: date-time }
                          terminationDate:
                            { type: [string, "null"], format: date-time }
                          residencyType:
                            type: string
                            enum:
                              ["Limited e-Resident", "e-Resident", "Resident"]
                          version: { type: string }
                      - type: "null"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/me/legal-entities:
    get:
      tags: [Me]
      summary: List the user's consented legal entities (OAuth only)
      security:
        - oauth2: [eprospera:entity.read]
      responses:
        "200":
          description: Legal entities the user consented to share
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/LegalEntitySummary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/me/legal-entities/{id}:
    get:
      tags: [Me]
      summary: Fetch one consented legal entity (OAuth only)
      security:
        - oauth2: [eprospera:entity.read]
      parameters:
        - $ref: "#/components/parameters/LegalEntityId"
      responses:
        "200":
          description: The legal-entity record
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LegalEntity" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/v1/me/legal-entities/{id}/documents:
    get:
      tags: [Me]
      summary: List documents for a consented legal entity (OAuth only)
      security:
        - oauth2: [eprospera:entity.documents.read]
      parameters:
        - $ref: "#/components/parameters/LegalEntityId"
      responses:
        "200":
          description: Documents
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Document" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/oauth/authorize:
    get:
      tags: [OAuth]
      summary: OAuth 2.0 authorization endpoint (RFC 6749 §4.1)
      security: []
      parameters:
        - in: query
          name: client_id
          required: true
          schema: { type: string }
        - in: query
          name: redirect_uri
          required: true
          schema: { type: string, format: uri }
        - in: query
          name: response_type
          required: true
          schema: { type: string, enum: [code] }
        - in: query
          name: scope
          required: true
          schema: { type: string }
        - in: query
          name: state
          required: true
          schema: { type: string }
        - in: query
          name: nonce
          required: false
          schema: { type: string }
          description: Required when `openid` is in `scope`.
        - in: query
          name: response_mode
          required: false
          schema:
            { type: string, enum: [query, fragment, form_post], default: query }
        - in: query
          name: code_challenge
          required: false
          schema: { type: string }
        - in: query
          name: code_challenge_method
          required: false
          schema: { type: string, enum: [S256] }
      responses:
        "302":
          description: |
            Redirects to the consent screen, then to `redirect_uri` with `code` and `state`
            (or `error=access_denied&state=...` on denial).

  /api/oauth/token:
    post:
      tags: [OAuth]
      summary: OAuth 2.0 token endpoint
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              oneOf:
                - type: object
                  required: [grant_type, code, redirect_uri]
                  properties:
                    grant_type: { type: string, enum: [authorization_code] }
                    code: { type: string }
                    redirect_uri: { type: string, format: uri }
                    code_verifier: { type: string }
                    client_id: { type: string }
                    client_secret: { type: string }
                - type: object
                  required: [grant_type, refresh_token]
                  properties:
                    grant_type: { type: string, enum: [refresh_token] }
                    refresh_token: { type: string }
                    scope: { type: string }
                    client_id: { type: string }
                    client_secret: { type: string }
      responses:
        "200":
          description: Token response (RFC 6749 §5.1)
          headers:
            Cache-Control:
              schema: { type: string, enum: [no-store] }
            Pragma:
              schema: { type: string, enum: [no-cache] }
          content:
            application/json:
              schema:
                type: object
                required: [access_token, token_type, expires_in, scope]
                properties:
                  access_token: { type: string }
                  token_type: { type: string, enum: [Bearer] }
                  expires_in: { type: integer, example: 3600 }
                  scope: { type: string }
                  id_token: { type: string }
                  refresh_token: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "500": { $ref: "#/components/responses/InternalError" }

  /api/oauth/userinfo:
    get:
      tags: [OAuth]
      summary: OIDC userinfo endpoint
      security:
        - oauth2: []
      responses:
        "200":
          description: Userinfo claims (subset depends on granted scopes)
          content:
            application/json:
              schema:
                type: object
                required: [sub]
                properties:
                  sub: { type: string }
                  name: { type: [string, "null"] }
                  given_name: { type: [string, "null"] }
                  family_name: { type: [string, "null"] }
                  picture: { type: [string, "null"], format: uri }
                  email: { type: [string, "null"], format: email }
                  email_verified: { type: boolean }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: "sk-... or ak-..."
      description: |
        Standard API key (`sk-...`) or Agent Key (`ak-...`). Agent Keys are scope-checked;
        see https://portal.eprospera.com/api-docs/agent-keys for the scope list.
    oauth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://portal.eprospera.com/api/oauth/authorize
          tokenUrl: https://portal.eprospera.com/api/oauth/token
          refreshUrl: https://portal.eprospera.com/api/oauth/token
          scopes:
            openid: OpenID Connect authentication.
            profile: name, given_name, family_name, picture.
            email: email, email_verified.
            offline_access: Issues a refresh token.
            "eprospera:person.details.read": Read personal details.
            "eprospera:person.residency.read": Read residency status.
            "eprospera:person.id_verification.read": Read ID-verification artifacts.
            "eprospera:entity.read": Read legal-entity details for consented entities.
            "eprospera:entity.documents.read": Read legal-entity documents for consented entities.

  parameters:
    LegalEntityId:
      in: path
      name: id
      required: true
      schema: { type: string, format: uuid }
    ApplicationId:
      in: path
      name: id
      required: true
      schema: { type: string, format: uuid }

  responses:
    BadRequest:
      description: Validation error or precondition failure.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    Unauthorized:
      description: Missing or invalid credential.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    Forbidden:
      description: Credential lacks the required scope.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    NotFound:
      description: Resource does not exist or is invisible to the caller.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    Conflict:
      description: Conflicting state (e.g. legal-entity name already taken).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    RateLimited:
      description: Rate limit exceeded.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    InternalError:
      description: Server error.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }

  schemas:
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error: { type: string }
        error_description: { type: string }
        details:
          type: array
          items:
            type: object

    Address:
      type: object
      properties:
        country: { type: string }
        line1: { type: string }
        line2: { type: [string, "null"] }
        city: { type: string }
        state: { type: [string, "null"] }
        postalCode: { type: string }

    LegalEntitySummary:
      type: object
      properties:
        id: { type: string, format: uuid }
        optionId: { type: string, example: "llc" }
        type: { type: string, example: "Limited Liability Company" }
        name: { type: string }
        extension: { type: string }
        nameStartsWithExtension: { type: boolean }
        residentPermitNumber: { type: [string, "null"] }

    LegalEntity:
      allOf:
        - $ref: "#/components/schemas/LegalEntitySummary"
        - type: object
          properties:
            formationDate: { type: [string, "null"], format: date-time }
            registrationDate: { type: [string, "null"], format: date-time }
            dissolutionDate: { type: [string, "null"], format: date-time }
            createdAt: { type: string, format: date-time }
            principalOfficeAddress:
              oneOf:
                - $ref: "#/components/schemas/Address"
                - type: "null"

    Document:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: [string, "null"] }
        version:
          oneOf:
            - { type: string }
            - { type: number }
        fileUrl: { type: string, format: uri }
        createdAt: { type: string, format: date-time }

    LegalEntityApplication:
      type: object
      properties:
        id: { type: string, format: uuid }
        statusId:
          type: string
          enum: ["Draft", "Pending Review", "Approved", "Rejected"]
        applicationData:
          type: object
          description: Stored internal format. Not the same shape as the create body.
        applicationVersion: { type: string }
        submittedAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
        approvedAt: { type: [string, "null"], format: date-time }
        rejectedAt: { type: [string, "null"], format: date-time }
        legalEntityId: { type: [string, "null"], format: uuid }

    CreateLegalEntityApplicationRequest:
      type: object
      required: [applicationData]
      properties:
        applicationData:
          type: object
          required:
            - residencyType
            - entityType
            - name
            - extension
            - principalOffice
            - contactEmail
          properties:
            residencyType:
              type: string
              enum: ["e-Resident", "Resident"]
            entityType:
              type: string
              enum: [llc]
            name: { type: string }
            extension:
              type: string
              enum:
                - LLC
                - L.L.C.
                - Limited Liability Company
                - S. de R.L.
                - SRL
                - Limited Company
                - L.C.
                - LC
                - Limited Liability Co.
                - Limited Co.
                - Ltd. Co.
            principalOffice: { $ref: "#/components/schemas/Address" }
            contactEmail: { type: string, format: email }
            registeredAgentProvider:
              type: [string, "null"]
              enum: [prospera_employment_solutions, null]
            registeredAgentDetails:
              oneOf:
                - type: object
                  properties:
                    attn: { type: string }
                    residentPermitNumber: { type: string }
                    officeAddress: { $ref: "#/components/schemas/Address" }
                    mailingAddress: { $ref: "#/components/schemas/Address" }
                - type: "null"
            analytics:
              type: object
              additionalProperties: true
        referralCode: { type: string }
        redirectUrl: { type: string, format: uri }

    NaturalPerson:
      type: object
      properties:
        givenName: { type: string }
        surname: { type: string }
        name: { type: string }
        residentPermitNumber: { type: [string, "null"] }
        countryOfBirth: { type: [string, "null"] }
        citizenships:
          type: array
          items: { type: string }
        dateOfBirth: { type: [string, "null"] }
        sex:
          type: [string, "null"]
          enum: [M, F, null]
        address:
          oneOf:
            - $ref: "#/components/schemas/Address"
            - type: "null"
        phoneNumber: { type: [string, "null"] }

    IdVerification:
      type: object
      properties:
        id: { type: [string, "null"] }
        type: { type: [string, "null"], enum: [veriff, null] }
        date: { type: [string, "null"], format: date-time }
        status: { type: [string, "null"], enum: [approved, null] }
        documents:
          type: object
          properties:
            documentFront: { type: [string, "null"], format: uri }
            documentBack: { type: [string, "null"], format: uri }
            face: { type: [string, "null"], format: uri }
