{
  "openapi": "3.0.3",
  "info": {
    "title": "Hookdeck Console Test URL API",
    "version": "0.1.0",
    "description": "Programmatic creation and inspection of Hookdeck Console \"Test URLs\" — real Hookdeck Sources that capture and let you inspect HTTP requests (RequestBin-style). Everything lives on the console.hookdeck.com host, served by Vercel (SPA + edge middleware + rewrites); create and ingestion are proxied to Hookdeck server-side, so callers only ever see console.hookdeck.com URLs (Hookdeck's public REST API at api.hookdeck.com is a separate public API). Create a Source with `POST /` (single JSON response) or the `GET /new` convenience link; the Source's ingestion URL is `console.hookdeck.com/e/<id>`, which captures requests of any HTTP verb (GET, POST, PUT, PATCH, DELETE); inspect a Source at `console.hookdeck.com/<id>`. Creating a Source unauthenticated provisions a guest workspace and sets a replayable session cookie (Domain=.hookdeck.com). This spec describes the target contract — see clients/apps/console/README.md — and some parts are still being built. This is an early beta (0.1.0); nothing has shipped and the contract may change.",
    "contact": {
      "name": "Hookdeck Support",
      "url": "https://hookdeck.com/contact-us",
      "email": "info@hookdeck.com"
    },
    "termsOfService": "https://hookdeck.com/terms"
  },
  "servers": [
    {
      "url": "https://console.hookdeck.com",
      "description": "Hookdeck Console"
    }
  ],
  "paths": {
    "/": {
      "get": {
        "operationId": "openConsole",
        "summary": "Open the Console",
        "description": "Serves the Hookdeck Console single-page application (served by Vercel from the console origin). Intended for interactive (browser) use.",
        "security": [],
        "responses": {
          "200": {
            "description": "The Console web application.",
            "content": { "text/html": {} }
          }
        }
      },
      "post": {
        "operationId": "createTestUrl",
        "summary": "Create a test URL (Source)",
        "description": "The canonical programmatic create. Creates a Hookdeck Source and returns explicit URL fields — `ingest_url` (the `/e/<id>` ingestion URL) and `inspect_url` (the `/<id>` bin) — plus the resolved `type`, the `name`, a `cli_key`, and a `cmd_hint` (a ready-to-run Hookdeck CLI command). All request fields are optional. The request is handled server-side and the Source is returned in a single response (no redirect to follow). Unauthenticated calls provision a guest workspace; replay the session cookie returned on the first call to create additional Sources in the same workspace.",
        "security": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateSourceRequest" },
              "example": { "type": "SHOPIFY", "name": "Example" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Source created (or, when the session cookie is replayed, a new Source in the same workspace).",
            "headers": {
              "Location": {
                "schema": { "type": "string", "format": "uri" },
                "description": "Canonical inspect URL of the created Source (console.hookdeck.com/<id>) — a GET there retrieves the Source (the Console app, or a JSON representation with `Accept: application/json`). Equals the `inspect_url` field in the response body; distinct from `ingest_url` (console.hookdeck.com/e/<id>)."
              },
              "Set-Cookie": {
                "schema": { "type": "string" },
                "description": "Session cookie (Domain=.hookdeck.com; SameSite=Strict) set on the first, unauthenticated call. Replay it to create further Sources in the same workspace."
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TestUrl" }
              }
            }
          },
          "422": {
            "description": "The provided `type` is not a supported Source Type (or is an auth-required type that cannot be used as an unauthenticated test URL).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UnprocessableEntityError" }
              }
            }
          },
          "429": {
            "description": "Source-creation rate limit exceeded.",
            "headers": {
              "Retry-After": {
                "schema": { "type": "integer" },
                "description": "Seconds to wait before retrying."
              },
              "X-RateLimit-Remaining": {
                "schema": { "type": "integer" },
                "description": "Remaining source creations in the current window."
              },
              "X-RateLimit-Reset": {
                "schema": { "type": "string", "format": "date-time" },
                "description": "When the current window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RateLimitError" }
              }
            }
          }
        }
      }
    },
    "/new": {
      "get": {
        "operationId": "createTestUrlAndOpen",
        "summary": "Create a Source and open it in the Console (browser)",
        "description": "A **browser-only** convenience entry point — a handy link a person opens to create a Source and land in the Console. The server returns the Console app (`200`); the app then creates a new Source and performs a **client-side redirect** to that Source's inspect path (`console.hookdeck.com/<id>`). Because the create happens in the browser, non-browser clients (curl, crawlers that don't run JS) get the app shell and create nothing. Accepts an optional `type` query parameter; unknown values are ignored (lenient). For programmatic create, use `POST /` — this route is not a programmatic API.",
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "type",
            "required": false,
            "schema": { "type": "string", "maxLength": 64 },
            "description": "Optional Source Type to pin (e.g. \"SHOPIFY\"). Unknown values are ignored (lenient, unlike the strict `POST /`)."
          }
        ],
        "responses": {
          "200": {
            "description": "The Console app. It creates a new Source in the browser and redirects (client-side) to the Source's inspect path (console.hookdeck.com/<id>).",
            "content": { "text/html": {} }
          }
        }
      }
    },
    "/e/{id}": {
      "description": "Ingestion for a Source. Console Sources accept all HTTP verbs (GET, POST, PUT, PATCH, DELETE) — RequestBin-style. Any method is captured as a Hookdeck Request; the HTTP response is the Source's configured response (200 by default). Inspecting a Source in a browser is a separate path (GET /{id}), so a GET to /e/ is always captured, never treated as inspection.",
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": { "type": "string" },
          "description": "The Source id without its `src_` prefix (the value embedded in the `url` returned by createTestUrl)."
        }
      ],
      "post": {
        "operationId": "ingestTestUrlPost",
        "summary": "Send a request to a test URL (POST)",
        "description": "Captures the request as a Hookdeck Request and delivers it through the Source's connections. The HTTP response is the Source's configured response (200 by default).",
        "security": [],
        "requestBody": { "required": false, "content": { "*/*": {} } },
        "responses": {
          "200": {
            "description": "Request captured. The status and body are the Source's configured response (200 by default)."
          }
        }
      },
      "get": {
        "operationId": "ingestTestUrlGet",
        "summary": "Send a request to a test URL (GET)",
        "description": "Captures a GET request. A GET to the `/e/` ingestion path is always captured; browser inspection is the separate `GET /{id}` path.",
        "security": [],
        "responses": {
          "200": {
            "description": "Request captured. The status and body are the Source's configured response (200 by default)."
          }
        }
      },
      "put": {
        "operationId": "ingestTestUrlPut",
        "summary": "Send a request to a test URL (PUT)",
        "description": "Captures the request. Response is the Source's configured response (200 by default).",
        "security": [],
        "requestBody": { "required": false, "content": { "*/*": {} } },
        "responses": {
          "200": { "description": "Request captured." }
        }
      },
      "patch": {
        "operationId": "ingestTestUrlPatch",
        "summary": "Send a request to a test URL (PATCH)",
        "description": "Captures the request. Response is the Source's configured response (200 by default).",
        "security": [],
        "requestBody": { "required": false, "content": { "*/*": {} } },
        "responses": {
          "200": { "description": "Request captured." }
        }
      },
      "delete": {
        "operationId": "ingestTestUrlDelete",
        "summary": "Send a request to a test URL (DELETE)",
        "description": "Captures the request. Response is the Source's configured response (200 by default).",
        "security": [],
        "responses": {
          "200": { "description": "Request captured." }
        }
      }
    },
    "/{id}": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": { "type": "string" },
          "description": "The Source id without its `src_` prefix (the value embedded in the `url` returned by createTestUrl)."
        }
      ],
      "get": {
        "operationId": "inspectTestUrl",
        "summary": "Inspect a Source",
        "description": "Opens a Source for inspection. The representation is chosen by the `Accept` header:\n\n- **`Accept: application/json`** → a JSON preview (`SourcePreview`) of the Source and its most recently captured requests, for programmatic inspection.\n- **Anything else** (a browser's `Accept: text/html`, or no `Accept`) → the Console app, deep-linked to this Source.\n\nInspection is a distinct namespace from ingestion (`/e/{id}`), so a GET here is never captured as an event.",
        "security": [],
        "parameters": [
          {
            "in": "header",
            "name": "Accept",
            "required": false,
            "schema": { "type": "string" },
            "description": "`application/json` for the JSON preview; otherwise (e.g. `text/html`, browser navigation) the Console app is served."
          }
        ],
        "responses": {
          "200": {
            "description": "The Console app (for a browser navigation) or a JSON preview of the Source and its recent captured requests (for `Accept: application/json`).",
            "content": {
              "text/html": {},
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SourcePreview" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CreateSourceRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "type": {
            "type": "string",
            "maxLength": 64,
            "description": "Optional Source Type to pin (UPPER CASE, e.g. \"SHOPIFY\", \"STRIPE\"). Omitted → a generic WEBHOOK source. A value that isn't a supported Source Type (or is an auth-required type) returns 422.",
            "example": "SHOPIFY"
          },
          "name": {
            "type": "string",
            "maxLength": 155,
            "description": "Optional source name. Defaults to \"Source\" and is auto-suffixed if a source with that name already exists in the workspace.",
            "example": "Example"
          }
        }
      },
      "TestUrl": {
        "type": "object",
        "required": ["ingest_url", "inspect_url", "type", "name", "cli_key", "cmd_hint"],
        "properties": {
          "ingest_url": {
            "type": "string",
            "format": "uri",
            "description": "The Source's ingestion URL (console.hookdeck.com/e/<id>). Deliver requests of any configured verb here to have them captured.",
            "example": "https://console.hookdeck.com/e/d12t0ffw5id1cm"
          },
          "inspect_url": {
            "type": "string",
            "format": "uri",
            "description": "The Source's inspection URL — the bin (console.hookdeck.com/<id>). GET it to view captured requests (the Console app, or JSON with `Accept: application/json`). Matches the `Location` header.",
            "example": "https://console.hookdeck.com/d12t0ffw5id1cm"
          },
          "type": {
            "type": "string",
            "description": "The resolved Source Type.",
            "example": "SHOPIFY"
          },
          "name": {
            "type": "string",
            "example": "Example"
          },
          "cli_key": {
            "type": "string",
            "description": "Key for authenticating the Hookdeck CLI against this source's workspace.",
            "example": "123123123"
          },
          "cmd_hint": {
            "type": "string",
            "description": "A ready-to-run Hookdeck CLI command with the cli_key filled in. `[port]` and `<source-name>` are placeholders to substitute.",
            "example": "npx hookdeck-cli listen [port] <source-name> --path /webhooks --cli-key 123123123"
          }
        }
      },
      "SourcePreview": {
        "type": "object",
        "description": "A preview of a Source and its most recently captured requests (RequestBin-style inspection). Shape is indicative — the inspection preview is still being built.",
        "required": ["id", "ingest_url", "type", "name", "requests"],
        "properties": {
          "id": {
            "type": "string",
            "description": "The Source id.",
            "example": "src_d12t0ffw5id1cm"
          },
          "ingest_url": {
            "type": "string",
            "format": "uri",
            "description": "The Source's ingestion URL (console.hookdeck.com/e/<id>).",
            "example": "https://console.hookdeck.com/e/d12t0ffw5id1cm"
          },
          "type": { "type": "string", "example": "SHOPIFY" },
          "name": { "type": "string", "example": "Example" },
          "requests": {
            "type": "array",
            "description": "Most recently captured requests, newest first.",
            "items": { "$ref": "#/components/schemas/CapturedRequest" }
          }
        }
      },
      "CapturedRequest": {
        "type": "object",
        "description": "A single captured request (an ingested HTTP request to the Source), from the Request plus its ShortEventData.",
        "properties": {
          "id": { "type": "string", "example": "req_9x2q1m0abc" },
          "verb": {
            "type": "string",
            "description": "Captured HTTP method (derived from the `x-hookdeck-original-method` header; defaults to POST).",
            "example": "POST"
          },
          "received_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the request was ingested.",
            "example": "2026-07-24T12:34:56Z"
          },
          "path": { "type": "string", "example": "/webhooks" },
          "query": { "type": "string", "example": "foo=bar" },
          "headers": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Captured request headers."
          },
          "body": {
            "description": "Captured request body (a truncated preview; string or JSON).",
            "oneOf": [{ "type": "string" }, { "type": "object" }],
            "nullable": true
          },
          "verified": {
            "type": "boolean",
            "description": "Whether the request passed the Source's verification."
          },
          "rejection_cause": {
            "type": "string",
            "nullable": true,
            "description": "Why the request was rejected, if any (e.g. NO_CONNECTION, UNSUPPORTED_HTTP_METHOD)."
          }
        }
      },
      "UnprocessableEntityError": {
        "type": "object",
        "properties": {
          "code": { "type": "string", "example": "UNPROCESSABLE_ENTITY" },
          "status": { "type": "integer", "example": 422 },
          "data": {
            "type": "array",
            "items": { "type": "string" },
            "description": "One or more validation messages.",
            "example": ["type must be one of [WEBHOOK, SHOPIFY, STRIPE]"]
          }
        }
      },
      "RateLimitError": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "example": "Too Many Requests" },
          "message": { "type": "string", "example": "Limited to 60 source creations per minute." },
          "retry_after": {
            "type": "integer",
            "description": "Seconds to wait before retrying.",
            "example": 30
          }
        }
      }
    }
  }
}
