> ## Documentation Index
> Fetch the complete documentation index at: https://jorgecastro.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# List published pages

> Lets Castro resolve which entity a crawled URL belongs to (bulk
updates match by slug). Return every publicly reachable page you want
Castro to be able to update.

Castro also uses this endpoint to sync page identity onto its crawl of
your site, so the richer each item is, the better. `categories` and
`tags` are optional — omit them and the sync still works, those fields
just stay empty in Castro.




## OpenAPI

````yaml /openapi/contract.yaml get /pages
openapi: 3.1.0
info:
  title: Custom Integration contract (v1)
  version: '1.0'
  summary: The REST endpoints you implement so Jorge Castro can publish to your site.
  description: >
    <div style="padding:12px 16px;border-left:3px solid
    #9827FC;background:rgba(152,39,252,0.06);border-radius:0 8px 8px
    0;margin-bottom:16px">

    <strong>Read this backwards.</strong> Jorge Castro is the <em>client</em>
    here —

    <strong>you implement these endpoints on your own server</strong>. Nothing
    below runs on

    our infrastructure.

    </div>


    When a user clicks Publish, Update or Delete in Jorge Castro, our servers
    send

    signed HTTPS requests to the base URL you registered (for example

    `https://your-site.com/api/castro`) using the paths below. Your
    implementation

    stores the content wherever your site reads it from, and returns the ids

    described here.


    ## Only three endpoints are required


    `POST /handshake`, `POST /posts` and `PUT /posts/{id}`. Everything else is

    optional: declare what you built in the handshake's `capabilities` array,
    and

    Castro only ever calls what you declared. See

    [Capabilities](/capabilities).


    ## Two rules that govern every endpoint


    **1. `PUT` is a partial update.** Only the fields present in the body
    change;

    omitted fields keep their current values. Castro's *Update Content* action

    sends nothing but `content` — treating that as a full replace erases the

    user's title, categories and SEO. Create and update therefore have

    **different schemas** below (`PostCreate` vs `PostUpdate`), and the
    difference

    is the whole contract.


    **2. Every request is signed.** Three headers travel with each one; verify
    the

    HMAC over the **raw** request bytes, never over a re-serialized copy. See

    [Authentication](/authentication).


    ## Before you connect


    Run the [conformance script](/testing) against your endpoints. It asserts
    both

    rules above, plus that a bad signature is actually rejected.
  contact:
    name: Jorge Castro
    url: https://jorgecastro.ai
servers:
  - url: https://your-site.com/{basePath}
    description: Your server — the base URL you enter in Jorge Castro
    variables:
      basePath:
        default: api/castro
        description: Any prefix you like. Castro appends the paths below to it.
security:
  - apiKey: []
    castroTimestamp: []
    castroSignature: []
tags:
  - name: Handshake
    description: Required — connection verification and capability discovery.
  - name: Blog posts
    description: Create and update are required; delete is optional (`posts.delete`).
  - name: Blog categories
    description: Optional — capability `blog_categories`.
  - name: Authors
    description: Optional — capability `authors`.
  - name: Products
    description: Optional — capability `products`. Copy only (no price/SKU/stock).
  - name: Product categories
    description: Optional — capability `product_categories`.
  - name: SEO
    description: Optional — capability `seo`.
  - name: Pages
    description: Optional — capability `pages.list`.
paths:
  /pages:
    get:
      tags:
        - Pages
      summary: List published pages
      description: |
        Lets Castro resolve which entity a crawled URL belongs to (bulk
        updates match by slug). Return every publicly reachable page you want
        Castro to be able to update.

        Castro also uses this endpoint to sync page identity onto its crawl of
        your site, so the richer each item is, the better. `categories` and
        `tags` are optional — omit them and the sync still works, those fields
        just stay empty in Castro.
      operationId: listPages
      parameters:
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/Page'
      responses:
        '200':
          description: Page list (bare JSON array)
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                    - id
                    - slug
                  properties:
                    id:
                      type: string
                    url:
                      type: string
                    slug:
                      type: string
                    title:
                      type: string
                    type:
                      type: string
                      description: |
                        Your own type label (post, product, page, ...). Castro
                        maps the well-known ones onto its page classification:
                        `product` -> Ecommerce Product Page,
                        `product_cat` -> Ecommerce Category Page,
                        `category` -> Category Page, `post` -> Blog Page.
                        Anything else leaves Castro's own classification intact.
                    categories:
                      type: array
                      description: Optional. Category names this page belongs to.
                      items:
                        type: string
                    tags:
                      type: array
                      description: Optional. Tag names on this page.
                      items:
                        type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
components:
  parameters:
    PerPage:
      name: per_page
      in: query
      description: Items per page. Castro asks for 100.
      schema:
        type: integer
        default: 100
    Page:
      name: page
      in: query
      description: |
        1-based page number. Castro keeps paging until it receives a page
        **shorter** than `per_page`, so a full final page must be followed by an
        empty one — otherwise it will keep asking.
      schema:
        type: integer
        default: 1
  responses:
    Unauthorized:
      description: |
        Authentication failed — bad API key, bad signature, or a stale
        timestamp. Castro surfaces this as a connection problem and does not
        retry.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            badSignature:
              summary: Signature mismatch
              value:
                error: Invalid signature
            stale:
              summary: Timestamp too old
              value:
                error: Stale request
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: |
            Human-readable message, surfaced to the Castro user as-is. Make it
            actionable — "Post not found" is useful, "Error" is not.
          example: Post not found
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        The connection key the user generated in Jorge Castro
        (Settings → Integration → Custom Website). Reject any request whose key
        doesn't match yours.
    castroTimestamp:
      type: apiKey
      in: header
      name: X-Castro-Timestamp
      description: |
        Unix time in **milliseconds** when the request was signed — Castro uses
        `Date.now()`. Reject anything more than a few minutes old.

        A seconds-based comparison makes every request look ~55,000 years in the
        future, and the freshness check then silently passes everything.
    castroSignature:
      type: apiKey
      in: header
      name: X-Castro-Signature
      description: |
        `HMAC-SHA256("{timestamp}.{rawBody}", api_key)`, lowercase hex.

        `rawBody` is the **exact bytes** of the request body — empty for `GET`
        and `DELETE`, which therefore sign over `"{timestamp}."`. Hash the raw
        bytes, never a re-serialized copy of the parsed JSON: whitespace and key
        order differences will break the comparison for some payloads and not
        others, which reads as an intermittent bug.

        Compare in constant time (`crypto.timingSafeEqual`, `hash_equals`,
        `hmac.compare_digest`).

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.