> ## 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.

# Update SEO metadata only

> Update ONLY the SEO block of an entity — nothing else changes. The
target is identified by `id` (the id you returned at create time),
`url` (the live page URL) or `slug`; resolve whichever you receive.




## OpenAPI

````yaml /openapi/contract.yaml put /seo
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:
  /seo:
    put:
      tags:
        - SEO
      summary: Update SEO metadata only
      description: |
        Update ONLY the SEO block of an entity — nothing else changes. The
        target is identified by `id` (the id you returned at create time),
        `url` (the live page URL) or `slug`; resolve whichever you receive.
      operationId: updateSeo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - seo
              properties:
                id:
                  type: string
                  description: Entity id, when Castro has one stored.
                url:
                  type: string
                  description: >-
                    Live page URL, when the content was generated from a crawled
                    page.
                  example: https://your-site.com/shoes/trail-runner-x
                slug:
                  type: string
                  example: trail-runner-x
                seo:
                  $ref: '#/components/schemas/Seo'
      responses:
        '200':
          description: SEO updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/Error'
components:
  schemas:
    Seo:
      type: object
      properties:
        title:
          type: string
        description:
          type: string
        keywords:
          type: array
          items:
            type: string
    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
  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
    Error:
      description: |
        Something went wrong. The `error` message is shown to the Castro user
        **verbatim**, so write it for a person.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            notFound:
              summary: Unknown id
              value:
                error: Post not found
            rejected:
              summary: Payload rejected
              value:
                error: A title is required
  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.