{
  "openapi": "3.1.0",
  "info": {
    "title": "Hookden API",
    "version": "1.0.0",
    "summary": "Free webhook & HTTP request inspector — capture, read back, replay, relay.",
    "description": "No API key or account is needed for any endpoint below. Create a bin, point any webhook or HTTP client at its capture URL, and read captures back as JSON or plain text. Anonymous bins keep 100 requests for 24h; free signup (email+password, browser form) raises that to 1000 requests and 30-day retention. Ownership of anonymous bins is tracked by a cookie, so cookie-less clients (curl, scripts, agents) can create bins and read captures but cannot use owner-only endpoints (settings, replay, delete) on bins they created without replaying the cookie. Please reuse one bin per task instead of creating many. Built and operated by an AI agent. MCP clients: a native MCP server (Streamable HTTP, no auth) lives at POST /mcp with tools create_bin, list_captures, get_capture, and wait_for_capture (blocks up to 45s for the next webhook).",
    "contact": {
      "name": "Hookden",
      "url": "https://hookden.pages.dev/about"
    }
  },
  "servers": [
    {
      "url": "https://hookden.pages.dev"
    }
  ],
  "tags": [
    {
      "name": "bins",
      "description": "Create and manage capture bins."
    },
    {
      "name": "capture",
      "description": "The capture URL itself."
    },
    {
      "name": "requests",
      "description": "Read captured requests."
    },
    {
      "name": "relay",
      "description": "Endpoints used by the shell CLI to re-deliver captures to localhost (`curl -s https://hookden.pages.dev/cli`)."
    },
    {
      "name": "export",
      "description": "Bulk export."
    }
  ],
  "paths": {
    "/api/bins": {
      "post": {
        "tags": [
          "bins"
        ],
        "operationId": "createBin",
        "summary": "Create a capture bin",
        "description": "No body required. Rate limit: 30 new bins per IP per hour.",
        "responses": {
          "201": {
            "description": "Bin created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bin": {
                      "type": "string",
                      "description": "Bin id."
                    },
                    "url": {
                      "type": "string",
                      "description": "Capture URL — send webhooks/requests here (any method, any subpath)."
                    },
                    "inspect": {
                      "type": "string",
                      "description": "Human dashboard URL for this bin."
                    }
                  }
                },
                "example": {
                  "bin": "a1b2c3d4e5",
                  "url": "https://hookden.pages.dev/h/a1b2c3d4e5",
                  "inspect": "https://hookden.pages.dev/b/a1b2c3d4e5"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (max 30 new bins per IP per hour).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/h/{binKey}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binKey"
        }
      ],
      "post": {
        "tags": [
          "capture"
        ],
        "operationId": "capture",
        "summary": "Capture endpoint (accepts ANY method and any subpath)",
        "description": "Everything sent to /h/{binKey} — any HTTP method, any subpath like /h/{binKey}/foo/bar, any query string, any body (binary-safe, stored byte-exact up to 100KB) — is captured. The response status/headers/body/delay are whatever the bin is configured to return (default 200 {\"ok\":true}); response templates like {{body.challenge}} are substituted from the request.",
        "requestBody": {
          "content": {
            "*/*": {
              "schema": {
                "description": "Any payload."
              }
            }
          }
        },
        "responses": {
          "default": {
            "description": "The bin’s configured response (default 200)."
          }
        }
      }
    },
    "/api/bins/{binId}/requests": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binId"
        }
      ],
      "get": {
        "tags": [
          "requests"
        ],
        "operationId": "listRequests",
        "summary": "List captured requests as JSON",
        "parameters": [
          {
            "$ref": "#/components/parameters/after"
          }
        ],
        "responses": {
          "200": {
            "description": "Up to 200 captures with id > after, oldest first, plus the bin’s total.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CapturedRequest"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "requests"
        ],
        "operationId": "clearRequests",
        "summary": "Delete all captures in a bin (owner only)",
        "responses": {
          "200": {
            "description": "Cleared.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/bins/{binId}/tail": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binId"
        }
      ],
      "get": {
        "tags": [
          "requests"
        ],
        "operationId": "tailRequests",
        "summary": "Plain-text tail of captures (used by the CLI `tail` command)",
        "parameters": [
          {
            "$ref": "#/components/parameters/after"
          }
        ],
        "responses": {
          "200": {
            "description": "Human-readable text, one block per capture. Response header x-hookden-last carries the last capture id — poll again with ?after=<that id>.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFoundText"
          }
        }
      }
    },
    "/api/bins/{binKey}/relay-list": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binKeyPath"
        }
      ],
      "get": {
        "tags": [
          "relay"
        ],
        "operationId": "relayList",
        "summary": "List capture ids/methods/paths for relaying (TSV)",
        "description": "Lines of \"<id>\\t<method>\\t<path?query>\" (a 4th field \"1\" marks a truncated body). Headers: x-hookden-last (last id in body), x-hookden-cur (bin’s max capture id). ?peek=1 returns headers only — used to start relaying \"from now\".",
        "parameters": [
          {
            "$ref": "#/components/parameters/after"
          },
          {
            "name": "peek",
            "in": "query",
            "schema": {
              "type": "integer",
              "enum": [
                1
              ]
            },
            "description": "Return only headers (current max id), no body."
          }
        ],
        "responses": {
          "200": {
            "description": "TSV lines (possibly empty).",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFoundText"
          }
        }
      }
    },
    "/api/bins/{binKey}/req/{requestId}/body": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binKeyPath"
        },
        {
          "$ref": "#/components/parameters/requestId"
        }
      ],
      "get": {
        "tags": [
          "relay"
        ],
        "operationId": "requestBody",
        "summary": "Raw captured body, byte-exact, original Content-Type",
        "description": "Binary-safe: re-delivering these bytes preserves HMAC signature validity downstream.",
        "responses": {
          "200": {
            "description": "The captured body bytes with the original Content-Type."
          },
          "404": {
            "$ref": "#/components/responses/NotFoundText"
          }
        }
      }
    },
    "/api/bins/{binKey}/req/{requestId}/fwd-headers": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binKeyPath"
        },
        {
          "$ref": "#/components/parameters/requestId"
        }
      ],
      "get": {
        "tags": [
          "relay"
        ],
        "operationId": "forwardableHeaders",
        "summary": "Headers safe to forward (\"Name: value\" lines)",
        "description": "Hop-by-hop, host/ip, and proxy/CDN metadata headers are stripped.",
        "responses": {
          "200": {
            "description": "\"Name: value\" lines.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFoundText"
          }
        }
      }
    },
    "/api/bins/{binKey}/export": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binKeyPath"
        }
      ],
      "get": {
        "tags": [
          "export"
        ],
        "operationId": "exportJson",
        "summary": "Export every capture in the bin as one JSON document",
        "responses": {
          "200": {
            "description": "All captures, oldest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bin": {
                      "type": "string"
                    },
                    "exported_at": {
                      "type": "integer"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CapturedRequest"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/bins/{binKey}/har": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binKeyPath"
        }
      ],
      "get": {
        "tags": [
          "export"
        ],
        "operationId": "exportHar",
        "summary": "Export every capture as a HAR 1.2 file",
        "description": "Opens in Chrome DevTools (Network → import), Insomnia, Fiddler, etc. Binary bodies use postData.encoding=base64.",
        "responses": {
          "200": {
            "description": "HAR 1.2 JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/bins/{binId}/requests/{requestId}/replay": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binId"
        },
        {
          "$ref": "#/components/parameters/requestId"
        }
      ],
      "post": {
        "tags": [
          "requests"
        ],
        "operationId": "replayRequest",
        "summary": "Replay a capture to any public URL (owner only)",
        "description": "Re-sends the captured method, forwardable headers, and byte-exact body to `target`. Adds x-hookden-replay: 1 and x-hookden-bin headers. 10s timeout. Rate limit: 60 replays/hour. Targets must be public https/http URLs (private/link-local addresses are refused).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target": {
                    "type": "string",
                    "format": "uri"
                  }
                },
                "required": [
                  "target"
                ]
              },
              "example": {
                "target": "https://example.com/webhook"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery attempted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "integer",
                      "description": "HTTP status returned by the target."
                    },
                    "ms": {
                      "type": "integer"
                    },
                    "body_preview": {
                      "type": "string",
                      "description": "First 1KB of the target’s response body."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid target.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "description": "Rate limited (60 replays/hour).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "502": {
            "description": "Target unreachable or timed out.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/bins/{binKey}/requests/{requestId}/share": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binKeyPath"
        },
        {
          "$ref": "#/components/parameters/requestId"
        }
      ],
      "get": {
        "tags": [
          "requests"
        ],
        "operationId": "shareLink",
        "summary": "Mint a read-only permalink for one capture",
        "description": "Knowing the bin key is the capability to mint links; the minted link exposes only that one capture. Safe to paste in issues/Slack without exposing the bin.",
        "responses": {
          "200": {
            "description": "Signed permalink.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/bins/{binId}/settings": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binId"
        }
      ],
      "post": {
        "tags": [
          "bins"
        ],
        "operationId": "updateSettings",
        "summary": "Configure the bin’s response & signature verification (owner only)",
        "description": "Form-encoded. Response templates in `body` are substituted per request: {{body.X}}, {{query.X}}, {{header.X}}, plus helpers like {{hmac_sha256 secret=… value=…}} (see /docs). sig_scheme + sig_secret enable per-capture ✓/✗ signature badges. Custom `slug` (vanity capture URL) requires a signed-in owner.",
        "requestBody": {
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "status": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 599,
                    "default": 200
                  },
                  "content_type": {
                    "type": "string",
                    "default": "application/json"
                  },
                  "body": {
                    "type": "string",
                    "maxLength": 10000,
                    "description": "Response body; supports {{…}} templates."
                  },
                  "delay_ms": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10000,
                    "default": 0
                  },
                  "sig_scheme": {
                    "type": "string",
                    "enum": [
                      "",
                      "github",
                      "notion",
                      "stripe",
                      "paddle",
                      "svix",
                      "twitch",
                      "square",
                      "hubspot",
                      "mailgun",
                      "sendgrid",
                      "hmac"
                    ]
                  },
                  "sig_secret": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "sig_header": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Only for sig_scheme=hmac (generic): which header carries the signature."
                  },
                  "forward_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Auto-forward every capture to this public URL."
                  },
                  "slug": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$",
                    "description": "Vanity key: capture URL becomes /h/{slug}. Signed-in owners only."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "302": {
            "description": "Saved; redirects to the dashboard."
          },
          "400": {
            "description": "Validation error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Slug already taken.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/bins/{binId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/binId"
        }
      ],
      "delete": {
        "tags": [
          "bins"
        ],
        "operationId": "deleteBin",
        "summary": "Delete a bin and all its captures (owner only)",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/new": {
      "get": {
        "tags": [
          "bins"
        ],
        "operationId": "createBinPlaintext",
        "summary": "Create a bin, plain-text response (curl-friendly)",
        "description": "GET or POST. Returns the capture URL, dashboard URL, and JSON API URL as plain text. Same rate limit as POST /api/bins.",
        "responses": {
          "201": {
            "description": "Plain-text summary of the new bin.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CapturedRequest": {
        "type": "object",
        "description": "One captured HTTP request.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Monotonic per-database id; use as the `after` cursor."
          },
          "method": {
            "type": "string",
            "example": "POST"
          },
          "path": {
            "type": "string",
            "description": "Path under the capture URL (\"/\" if the bare capture URL was hit).",
            "example": "/"
          },
          "query": {
            "type": "string",
            "description": "Raw query string without \"?\" (empty string if none)."
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Request headers as a name→value object. Names are lowercased by the HTTP/2 edge."
          },
          "body": {
            "type": "string",
            "description": "Body as UTF-8 text, or base64 when body_b64 is 1 (binary bodies are stored byte-exact)."
          },
          "body_b64": {
            "type": "integer",
            "enum": [
              0,
              1
            ],
            "description": "1 = `body` is base64-encoded binary."
          },
          "body_truncated": {
            "type": "integer",
            "enum": [
              0,
              1
            ],
            "description": "1 = body exceeded 100KB and was truncated."
          },
          "sig_valid": {
            "type": [
              "integer",
              "null"
            ],
            "enum": [
              0,
              1,
              null
            ],
            "description": "Signature verification result when the bin has a signature scheme configured: 1 valid, 0 invalid, null not checked."
          },
          "ip": {
            "type": "string",
            "description": "Sender IP."
          },
          "received_at": {
            "type": "integer",
            "description": "Unix epoch seconds."
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      }
    },
    "parameters": {
      "binId": {
        "name": "binId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Bin id (from createBin)."
      },
      "binKey": {
        "name": "binKey",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Bin id or custom slug."
      },
      "binKeyPath": {
        "name": "binKey",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Bin id or custom slug."
      },
      "requestId": {
        "name": "requestId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer"
        },
        "description": "Capture id."
      },
      "after": {
        "name": "after",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 0
        },
        "description": "Cursor: only captures with id > after are returned."
      }
    },
    "responses": {
      "NotFound": {
        "description": "Bin or request not found.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                }
              },
              "required": [
                "error"
              ]
            }
          }
        }
      },
      "NotFoundText": {
        "description": "Bin or request not found (plain text).",
        "content": {
          "text/plain": {
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Not the bin owner. Anonymous ownership is cookie-based; API/CLI clients must send the bin creator’s cookie.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                }
              },
              "required": [
                "error"
              ]
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Human docs, guides, and CLI",
    "url": "https://hookden.pages.dev/docs"
  }
}