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

# Verify the connection

> Called once when the user connects (or re-verifies) their website in
Castro. Prove you hold the API key by returning
`challenge_response = HMAC-SHA256(challenge, api_key)` as lowercase
hex, and declare which optional endpoints you implemented.

The connection is rejected unless `capabilities` includes both
`posts.create` and `posts.update`.




## OpenAPI

````yaml /openapi/contract.yaml post /handshake
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:
  /handshake:
    post:
      tags:
        - Handshake
      summary: Verify the connection
      description: |
        Called once when the user connects (or re-verifies) their website in
        Castro. Prove you hold the API key by returning
        `challenge_response = HMAC-SHA256(challenge, api_key)` as lowercase
        hex, and declare which optional endpoints you implemented.

        The connection is rejected unless `capabilities` includes both
        `posts.create` and `posts.update`.
      operationId: handshake
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - challenge
              properties:
                challenge:
                  type: string
                  description: Random hex string to sign with your API key.
                  example: >-
                    3f9a1c62b6d94f0e8a17c54b9d2e6f38a1b0c9d8e7f6a5b4c3d2e1f009876543
      responses:
        '200':
          description: Connection accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HandshakeResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
components:
  schemas:
    HandshakeResponse:
      type: object
      required:
        - capabilities
        - challenge_response
      properties:
        name:
          type: string
          description: Your integration's display name.
          example: Acme Store Backend
        version:
          type: string
          example: '1.0'
        capabilities:
          type: array
          description: |
            Which endpoints you implemented. MUST include `posts.create` and
            `posts.update`.
          items:
            type: string
            enum:
              - posts.create
              - posts.update
              - posts.delete
              - blog_categories
              - authors
              - products
              - product_categories
              - seo
              - pages.list
        challenge_response:
          type: string
          description: >-
            HMAC-SHA256 of the challenge, keyed with your API key, lowercase
            hex.
    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
  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.