openapi: "3.0.2"
info:
  title: Page Server API
  description: Neon Pageserver API
  version: "1.0"
  license:
    name: "Apache"
    url: https://github.com/neondatabase/neon/blob/main/LICENSE
servers:
  - url: ""
paths:
  /v1/status:
    description: Healthcheck endpoint
    get:
      description: Healthcheck
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                properties:
                  id:
                    type: integer

  /v1/disk_usage_eviction/run:
    put:
      description: Do an iteration of disk-usage-based eviction to evict a given amount of disk space.
      security: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - evict_bytes
              properties:
                evict_bytes:
                  type: integer
      responses:
        "200":
          description: |
            The run completed.
            This does not necessarily mean that we actually evicted `evict_bytes`.
            Examine the returned object for detail, or, just watch the actual effect of the call using `du` or `df`.
          content:
            application/json:
              schema:
                type: object

  /v1/reload_auth_validation_keys:
    post:
      description: Reloads the JWT public keys from their pre-configured location on disk.
      responses:
        "200":
          description: The reload completed successfully.

  /v1/tenant/{tenant_id}:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
    get:
      description: Get tenant status
      responses:
        "200":
          description: Currently returns the flag whether the tenant has inprogress timeline downloads
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantInfo"

    delete:
      description: |
        Attempts to delete specified tenant. 500, 503 and 409 errors should be retried until 404 is retrieved.
        404 means that deletion successfully finished"
      responses:
        "404":
          description: Tenant not found. This is the success path.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotFoundError"
        "409":
          description: Deletion is already in progress, continue polling
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConflictError"
        "412":
          description: Deletion may not proceed, tenant is not in Active state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PreconditionFailedError"

  /v1/tenant/{tenant_id}/time_travel_remote_storage:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
      - name: travel_to
        in: query
        required: true
        schema:
          type: string
          format: date-time
      - name: done_if_after
        in: query
        required: true
        schema:
          type: string
          format: date-time
    put:
      description: Time travel the tenant's remote storage
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: string

  /v1/tenant/{tenant_id}/timeline:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
    get:
      description: Get timelines for tenant
      responses:
        "200":
          description: TimelineInfo
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TimelineInfo"


  /v1/tenant/{tenant_id}/timeline/{timeline_id}:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
      - name: timeline_id
        in: path
        required: true
        schema:
          type: string
          format: hex
    get:
      description: Get info about the timeline
      responses:
        "200":
          description: TimelineInfo
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimelineInfo"

    delete:
      description: "Attempts to delete specified timeline. 500 and 409 errors should be retried"
      responses:
        "404":
          description: Timeline not found. This is the success path.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotFoundError"
        "409":
          description: Deletion is already in progress, continue polling
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConflictError"
        "412":
          description: Tenant is missing, or timeline has children
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PreconditionFailedError"

  /v1/tenant/{tenant_id}/timeline/{timeline_id}/get_timestamp_of_lsn:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
      - name: timeline_id
        in: path
        required: true
        schema:
          type: string
          format: hex
    get:
      description: Get timestamp for a given LSN
      parameters:
        - name: lsn
          in: query
          required: true
          schema:
            type: string
            format: hex
          description: A LSN to get the timestamp
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: string
                format: date-time

  /v1/tenant/{tenant_id}/timeline/{timeline_id}/get_lsn_by_timestamp:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
      - name: timeline_id
        in: path
        required: true
        schema:
          type: string
          format: hex
    get:
      description: Get LSN by a timestamp
      parameters:
        - name: timestamp
          in: query
          required: true
          schema:
            type: string
            format: date-time
          description: A timestamp to get the LSN
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LsnByTimestampResponse"

  /v1/tenant/{tenant_id}/timeline/{timeline_id}/do_gc:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
      - name: timeline_id
        in: path
        required: true
        schema:
          type: string
          format: hex
    put:
      description: Garbage collect given timeline
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: string
  /v1/tenant/{tenant_shard_id}/location_config:
    parameters:
      - name: tenant_shard_id
        in: path
        required: true
        schema:
          type: string
      - name: flush_ms
        in: query
        required: false
        schema:
          type: integer
      - name: lazy
        in: query
        required: false
        schema:
          type: boolean
        description: Set to true for attaches to queue up until activated by compute. Eager (false) is the default.
    put:
      description: |
        Configures a _tenant location_, that is how a particular pageserver handles
        a particular tenant.  This includes _attached_ tenants, i.e. those ingesting WAL
        and page service requests, and _secondary_ tenants, i.e. those which are just keeping
        a warm cache in anticipation of transitioning to attached state in the future.

        This is a declarative, idempotent API: there are not separate endpoints
        for different tenant location configurations.  Rather, this single endpoint accepts
        a description of the desired location configuration, and makes whatever changes
        are required to reach that state.

        In imperative terms, this API is used to attach and detach tenants, and
        to transition tenants to and from secondary mode.

        This is a synchronous API: there is no 202 response.  State transitions should always
        be fast (milliseconds), with the exception of requests setting `flush_ms`, in which case
        the caller controls the runtime of the request.

        In some state transitions, it makes sense to flush dirty data to remote storage: this includes transitions
        to AttachedStale and Detached.  Flushing is never necessary for correctness, but is an
        important optimization when doing migrations.  The `flush_ms` parameter controls whether
        flushing should be attempted, and how much time is allowed for flushing.  If the time limit expires,
        the requested transition will continue without waiting for any outstanding data to flush.  Callers
        should use a duration which is substantially less than their HTTP client's request
        timeout.  It is safe to supply flush_ms irrespective of the request body: in state transitions
        where flushing doesn't make sense, the server will ignore it.

        It is safe to retry requests, but if one receives a 409 or 503 response, it is not
        useful to retry aggressively: there is probably an existing request still ongoing.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TenantLocationConfigRequest"
      responses:
        "200":
          description: Tenant is now in requested state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantLocationConfigResponse"
        "409":
          description: |
            The tenant is already known to Pageserver in some way,
            and hence this `/attach` call has been rejected.

            Some examples of how this can happen:
            - tenant was created on this pageserver
            - tenant attachment was started by an earlier call to `/attach`.

            Callers should poll the tenant status's `attachment_status` field,
            like for status 202. See the longer description for `POST /attach`
            for details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConflictError"
  /v1/tenant/{tenant_id}/ignore:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
    post:
      description: |
        Remove tenant data (including all corresponding timelines) from pageserver's memory.
        Files on local disk and remote storage are not affected.

        Future pageserver restarts won't load the data back until `load` is called on such tenant.
      responses:
        "200":
          description: Tenant ignored


  /v1/tenant/{tenant_id}/load:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
    post:
      description: |
        Schedules an operation that attempts to load a tenant from the local disk and
        synchronise it with the remote storage (if enabled), repeating pageserver's restart logic for tenant load.
        If the tenant was ignored before, removes the ignore mark and continues with load scheduling.

        Errors if the tenant is absent on disk, already present in memory or fails to schedule its load.
        Scheduling a load does not mean that the tenant would load successfully, check tenant status to ensure load correctness.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TenantLoadRequest"
      responses:
        "202":
          description: Tenant scheduled to load successfully

  /v1/tenant/{tenant_id}/{timeline_id}/preserve_initdb_archive:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
      - name: timeline_id
        in: path
        required: true
        schema:
          type: string
    post:
      description: |
        Marks the initdb archive for preservation upon deletion of the timeline or tenant.
        This is meant to be part of the disaster recovery process.
      responses:
        "202":
          description: Tenant scheduled to load successfully

  /v1/tenant/{tenant_id}/synthetic_size:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
    get:
      description: |
        Calculate tenant's synthetic size
      responses:
        "200":
          description: Tenant's synthetic size
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SyntheticSizeResponse"

  # This route has no handler. TODO: remove?
  /v1/tenant/{tenant_id}/size:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
      - name: inputs_only
        in: query
        required: false
        schema:
          type: boolean
        description: |
          When true, skip calculation and only provide the model inputs (for debugging). Defaults to false.
      - name: retention_period
        in: query
        required: false
        schema:
          type: integer
        description: |
          Override the default retention period (in bytes) used for size calculation.
    get:
      description: |
        Calculate tenant's size, which is a mixture of WAL (bytes) and logical_size (bytes).
      responses:
        "200":
          description: OK,
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - size
                properties:
                  id:
                    type: string
                    format: hex
                  size:
                    type: integer
                    nullable: true
                    description: |
                      Size metric in bytes or null if inputs_only=true was given.
        "401":
          description: Unauthorized Error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UnauthorizedError"
        "403":
          description: Forbidden Error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ForbiddenError"
        "500":
          description: Generic operation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Temporarily unavailable, please retry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceUnavailableError"

  /v1/tenant/{tenant_shard_id}/heatmap_upload:
    parameters:
      - name: tenant_shard_id
        in: path
        required: true
        schema:
          type: string
    post:
      description: |
        If the location is in an attached mode, upload the current state to the remote heatmap
      responses:
        "200":
          description: Success

  /v1/tenant/{tenant_shard_id}/secondary/download:
    parameters:
      - name: tenant_shard_id
        in: path
        required: true
        schema:
          type: string
      - name: wait_ms
        description: If set, we will wait this long for download to complete, and if it isn't complete then return 202
        in: query
        required: false
        schema:
          type: integer
    post:
      description: |
        If the location is in secondary mode, download latest heatmap and layers
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SecondaryProgress"
        "202":
          description: Download has started but not yet finished
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SecondaryProgress"

  /v1/tenant/{tenant_id}/timeline/:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
    post:
      description: |
        Create a timeline. Returns new timeline id on success.
        Recreating the same timeline will succeed if the parameters match the existing timeline.
        If no pg_version is specified, assume DEFAULT_PG_VERSION hardcoded in the pageserver.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - new_timeline_id
              properties:
                new_timeline_id:
                  type: string
                  format: hex
                ancestor_timeline_id:
                  type: string
                  format: hex
                ancestor_start_lsn:
                  type: string
                  format: hex
                pg_version:
                  type: integer
                existing_initdb_timeline_id:
                  type: string
                  format: hex
      responses:
        "201":
          description: Timeline was created, or already existed with matching parameters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimelineInfo"
        "406":
          description: Permanently unsatisfiable request, don't retry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Timeline already exists, with different parameters.  Creation cannot proceed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConflictError"
        "429":
          description: A creation request was sent for the same Timeline Id while a creation was already in progress.  Back off and retry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v1/tenant/:
    get:
      description: Get tenants list
      responses:
        "200":
          description: TenantInfo
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TenantInfo"

    post:
      description: |
        Create a tenant. Returns new tenant id on success.

        If no new tenant id is specified in parameters, it would be generated. It's an error to recreate the same tenant.

        Invalid fields in the tenant config will cause the request to be rejected with status 400.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TenantCreateRequest"
      responses:
        "201":
          description: New tenant created successfully
          content:
            application/json:
              schema:
                type: string
        "409":
          description: Tenant already exists, creation skipped
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConflictError"

  /v1/tenant/config:
    put:
      description: |
        Update tenant's config.

        Invalid fields in the tenant config will cause the request to be rejected with status 400.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TenantConfigRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TenantInfo"

  /v1/tenant/{tenant_id}/config/:
    parameters:
      - name: tenant_id
        in: path
        required: true
        schema:
          type: string
    get:
      description: |
        Returns tenant's config description: specific config overrides a tenant has
        and the effective config.
      responses:
        "200":
          description: Tenant config, specific and effective
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantConfigResponse"

  /v1/utilization:
    get:
      description: |
        Returns the pageservers current utilization and fitness score for new tenants.

      responses:
        "200":
            description: Pageserver utilization and fitness score
            content:
              application/json:
                schema:
                  $ref: "#/components/schemas/PageserverUtilization"

components:
  securitySchemes:
    JWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    TenantInfo:
      type: object
      required:
        - id
        - attachment_status
      properties:
        id:
          type: string
        current_physical_size:
          type: integer
        attachment_status:
          description: |
            Status of this tenant's attachment to this pageserver.

            - `maybe` means almost nothing, don't read anything into it
              except for the fact that the pageserver _might_ be already
              writing to the tenant's S3 state, so, DO NOT ATTACH the
              tenant to any other pageserver, or we risk split-brain.
            - `attached` means that the attach operation has completed,
              successfully
            - `failed` means that attach has failed. For reason check corresponding `reason` failed.
              `failed` is the terminal state, retrying attach call wont resolve the issue.
              For example this can be caused by s3 being unreachable. The retry may be implemented
              with call to detach, though it would be better to not automate it and inspec failed state
              manually before proceeding with a retry.

            See the tenant `/attach` endpoint for more information.
          type: object
          required:
            - slug
            - data
          properties:
            slug:
              type: string
              enum: [ "maybe", "attached", "failed" ]
            data:
              type: object
              properties:
                reason:
                  type: string

    TenantCreateRequest:
      allOf:
        - $ref: '#/components/schemas/TenantConfig'
        - type: object
          required:
            - new_tenant_id
          properties:
            new_tenant_id:
              type: string
            generation:
              type: integer
              description: Attachment generation number.
    TenantLoadRequest:
      type: object
      properties:
        generation:
          type: integer
          description: Attachment generation number.
    TenantConfigRequest:
      allOf:
        - $ref: '#/components/schemas/TenantConfig'
        - type: object
          required:
            - tenant_id
          properties:
            tenant_id:
              type: string
    TenantLocationConfigRequest:
      type: object
      required:
        - mode
      properties:
        tenant_id:
          type: string
          description: Not used, scheduled for removal.
        mode:
          type: string
          enum: ["AttachedSingle", "AttachedMulti", "AttachedStale", "Secondary", "Detached"]
          description: Mode of functionality that this pageserver will run in for this tenant.
        generation:
          type: integer
          description: Attachment generation number, mandatory when `mode` is an attached state
        secondary_conf:
          $ref: '#/components/schemas/SecondaryConfig'
        tenant_conf:
          $ref: '#/components/schemas/TenantConfig'
    TenantLocationConfigResponse:
      type: object
      required:
        - shards
      properties:
        shards:
          description: Pageservers where this tenant's shards are attached.  Not populated for secondary locations.
          type: array
          items:
            $ref: "#/components/schemas/TenantShardLocation"
        stripe_size:
          description: If multiple shards are present, this field contains the sharding stripe size, else it is null.
          type: integer
          nullable: true
    TenantShardLocation:
      type: object
      required:
        - node_id
        - shard_id
      properties:
        node_id:
          description: Pageserver node ID where this shard is attached
          type: integer
        shard_id:
          description: Tenant shard ID of the shard
          type: string
    SecondaryConfig:
      type: object
      properties:
        warm:
          type: boolean
          description: Whether to poll remote storage for layers to download.  If false, secondary locations don't download anything.
    TenantConfig:
      type: object
      properties:
        gc_period:
          type: string
        gc_horizon:
          type: integer
        pitr_interval:
          type: string
        checkpoint_distance:
          type: integer
        checkpoint_timeout:
          type: string
        compaction_target_size:
          type: integer
        compaction_period:
          type: string
        compaction_threshold:
          type: string
        image_creation_threshold:
          type: integer
        walreceiver_connect_timeout:
          type: string
        lagging_wal_timeout:
          type: string
        max_lsn_wal_lag:
          type: integer
        trace_read_requests:
          type: boolean
        heatmap_period:
          type: string
    TenantConfigResponse:
      type: object
      properties:
        tenant_specific_overrides:
          $ref: "#/components/schemas/TenantConfig"
        effective_config:
          $ref: "#/components/schemas/TenantConfig"
    TimelineInfo:
      type: object
      required:
        - timeline_id
        - tenant_id
        - last_record_lsn
        - disk_consistent_lsn
        - state
        - latest_gc_cutoff_lsn
      properties:
        timeline_id:
          type: string
          format: hex
        tenant_id:
          type: string
        last_record_lsn:
          type: string
          format: hex
        disk_consistent_lsn:
          type: string
          format: hex
        remote_consistent_lsn:
          type: string
          format: hex
        remote_consistent_lsn_visible:
          type: string
          format: hex
        ancestor_timeline_id:
          type: string
          format: hex
        ancestor_lsn:
          type: string
          format: hex
        prev_record_lsn:
          type: string
          format: hex
        current_logical_size:
          type: integer
        current_physical_size:
          type: integer
        wal_source_connstr:
          type: string
        last_received_msg_lsn:
          type: string
          format: hex
        last_received_msg_ts:
          type: integer
        state:
          type: string
        latest_gc_cutoff_lsn:
          type: string
          format: hex

    SyntheticSizeResponse:
      type: object
      required:
        - id
        - size
        - segment_sizes
        - inputs
      properties:
        id:
          type: string
          format: hex
        size:
          type: integer
        segment_sizes:
          type: array
          items:
            $ref: "#/components/schemas/SegmentSize"
        inputs:
          type: object
          properties:
            segments:
              type: array
              items:
                $ref: "#/components/schemas/SegmentData"
            timeline_inputs:
              type: array
              items:
                $ref: "#/components/schemas/TimelineInput"

    SegmentSize:
      type: object
      required:
        - method
        - accum_size
      properties:
        method:
          type: string
        accum_size:
          type: integer

    SegmentData:
      type: object
      required:
        - segment
      properties:
        segment:
          type: object
          required:
            - lsn
          properties:
            parent:
              type: integer
            lsn:
              type: integer
            size:
              type: integer
            needed:
              type: boolean
        timeline_id:
          type: string
          format: hex
        kind:
          type: string

    TimelineInput:
      type: object
      required:
        - timeline_id
      properties:
        ancestor_id:
          type: string
        ancestor_lsn:
          type: string
        timeline_id:
          type: string
          format: hex

    LsnByTimestampResponse:
      type: object
      required:
        - lsn
        - kind
      properties:
        lsn:
          type: string
          format: hex
        kind:
          type: string
          enum: [past, present, future, nodata]

    PageserverUtilization:
      type: object
      required:
        - disk_usage_bytes
        - free_space_bytes
        - utilization_score
      properties:
        disk_usage_bytes:
          type: integer
          format: int64
          minimum: 0
          description: The amount of disk space currently used.
        free_space_bytes:
          type: integer
          format: int64
          minimum: 0
          description: The amount of usable disk space left.
        utilization_score:
          type: integer
          format: int64
          minimum: 0
          maximum: 9223372036854775807
          default: 9223372036854775807
          description: |
            Lower is better score for how good this pageserver would be for the next tenant.
            The default or maximum value can be returned in situations when a proper score cannot (yet) be calculated.

    SecondaryProgress:
      type: object
      required:
        - heatmap_mtime
        - layers_downloaded
        - layers_total
        - bytes_downloaded
        - bytes_total
      properties:
        heatmap_mtime:
          type: string
          format: date-time
          description: Modification time of the most recently downloaded layer heatmap (RFC 3339 format)
        layers_downloaded:
          type: integer
          format: int64
          description: How many layers from the latest layer heatmap are present on disk
        bytes_downloaded:
          type: integer
          format: int64
          description: How many bytes of layer content from the latest layer heatmap are present on disk
        layers_total:
          type: integer
          format: int64
          description: How many layers were in the latest layer heatmap
        bytes_total:
          type: integer
          format: int64
          description: How many bytes of layer content were in the latest layer heatmap


    Error:
      type: object
      required:
        - msg
      properties:
        msg:
          type: string
    UnauthorizedError:
      type: object
      required:
        - msg
      properties:
        msg:
          type: string
    ForbiddenError:
      type: object
      required:
        - msg
      properties:
        msg:
          type: string
    ServiceUnavailableError:
      type: object
      required:
        - msg
      properties:
        msg:
          type: string
    NotFoundError:
      type: object
      required:
        - msg
      properties:
        msg:
          type: string
    ConflictError:
      type: object
      required:
        - msg
      properties:
        msg:
          type: string
    PreconditionFailedError:
      type: object
      required:
        - msg
      properties:
        msg:
          type: string

security:
  - JWT: []
