{
  "openapi": "3.1.0",
  "info": {
    "title": "CatchHook API",
    "version": "1.0.0",
    "description": "REST API for CatchHook — a webhook and inbound email debugging platform. Manage endpoints, inspect events, configure Endpoint Actions, and connect localhost tunnels.",
    "contact": {
      "name": "CatchHook",
      "url": "https://catchhook.app"
    }
  },
  "servers": [
    {
      "url": "https://catchhook.app/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/auth/verify": {
      "get": {
        "summary": "Verify authentication",
        "description": "Returns the current user and account information. Useful for verifying your token is valid.",
        "operationId": "verifyAuth",
        "tags": ["Authentication"],
        "responses": {
          "200": {
            "description": "Token is valid",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "integer" },
                        "email": { "type": "string", "format": "email" }
                      }
                    },
                    "account": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "integer" },
                        "name": { "type": "string" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/endpoints": {
      "get": {
        "summary": "List endpoints",
        "description": "Returns all endpoints accessible to the authenticated user.",
        "operationId": "listEndpoints",
        "tags": ["Endpoints"],
        "responses": {
          "200": {
            "description": "List of endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Endpoint" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "summary": "Create an endpoint",
        "description": "Creates a new webhook endpoint. Endpoints created with a provider get automatic event type detection and provider-specific UI.",
        "operationId": "createEndpoint",
        "tags": ["Endpoints"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["endpoint"],
                "properties": {
                  "endpoint": {
                    "type": "object",
                    "required": ["name"],
                    "properties": {
                      "name": {
                        "type": "string",
                        "description": "Human-readable label"
                      },
                      "provider": {
                        "type": "string",
                        "enum": ["github", "stripe", "shopify", "slack", "twilio"],
                        "description": "Optional provider preset for enhanced event detection and display"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Endpoint created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/Endpoint" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/endpoints/{id}": {
      "get": {
        "summary": "Get an endpoint",
        "description": "Returns details for a single endpoint.",
        "operationId": "getEndpoint",
        "tags": ["Endpoints"],
        "parameters": [
          { "$ref": "#/components/parameters/EndpointId" }
        ],
        "responses": {
          "200": {
            "description": "Endpoint details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/Endpoint" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "summary": "Delete an endpoint",
        "description": "Permanently deletes an endpoint and all its requests. Requires write scope.",
        "operationId": "deleteEndpoint",
        "tags": ["Endpoints"],
        "parameters": [
          { "$ref": "#/components/parameters/EndpointId" }
        ],
        "responses": {
          "200": { "description": "Endpoint deleted" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/endpoints/{endpoint_id}/requests": {
      "get": {
        "summary": "List requests for an endpoint",
        "description": "Returns webhook requests received by the specified endpoint.",
        "operationId": "listRequests",
        "tags": ["Webhook Requests"],
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Endpoint ID (e.g., ep_abc123)"
          }
        ],
        "responses": {
          "200": {
            "description": "List of webhook requests",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/WebhookRequest" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/endpoints/{endpoint_id}/requests/{id}": {
      "get": {
        "summary": "Get a single request",
        "description": "Returns full details for a single webhook request, including the complete body.",
        "operationId": "getRequest",
        "tags": ["Webhook Requests"],
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Request ID (e.g., wr_def456)"
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook request details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/WebhookRequest" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "summary": "Delete a request",
        "description": "Permanently deletes a single webhook request. Requires write scope.",
        "operationId": "deleteRequest",
        "tags": ["Webhook Requests"],
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "Request deleted" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/endpoints/{endpoint_id}/requests/destroy_all": {
      "delete": {
        "summary": "Delete requests in bulk",
        "description": "Deletes all requests for the endpoint. Supports optional time range filters. Requires write scope.",
        "operationId": "deleteAllRequests",
        "tags": ["Webhook Requests"],
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "before",
            "in": "query",
            "schema": { "type": "string", "format": "date-time" },
            "description": "Delete requests received before this ISO 8601 timestamp"
          },
          {
            "name": "after",
            "in": "query",
            "schema": { "type": "string", "format": "date-time" },
            "description": "Delete requests received after this ISO 8601 timestamp"
          }
        ],
        "responses": {
          "200": { "description": "Requests deleted" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/endpoints/{endpoint_id}/requests/{id}/replay": {
      "post": {
        "summary": "Replay a request",
        "description": "Re-sends the webhook to a target URL with the original method, headers, and body. Supports identity regeneration and signature re-signing. Request modification (body, header, and query editing) is available in the web UI and will be added to the API in a future release. Requires write scope.",
        "operationId": "replayRequest",
        "tags": ["Webhook Requests"],
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["target_url"],
                "properties": {
                  "target_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "URL to send the replayed webhook to"
                  },
                  "identity_mode": {
                    "type": "string",
                    "enum": ["original", "regenerate"],
                    "default": "original",
                    "description": "Identity handling. 'regenerate' appends unique suffixes to common ID fields in JSON bodies to bypass idempotency checks. Requires a payment method."
                  },
                  "signature_mode": {
                    "type": "string",
                    "enum": ["strip", "resign"],
                    "default": "strip",
                    "description": "Signature handling. 'strip' removes provider signature headers. 'resign' computes a fresh signature using the endpoint's signing secret. Requires a payment method and supported signature config."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Request replayed successfully" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Forbidden — account lacks required entitlement (e.g., payment method for regenerate/resign)" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "description": "Validation error — invalid mode, unsupported provider for resign, or non-JSON body for regenerate" }
        }
      }
    },
    "/endpoints/{endpoint_id}/signature_configs": {
      "post": {
        "summary": "Create a signature config",
        "description": "Creates a signature verification config for an endpoint. Requires write scope.",
        "operationId": "createSignatureConfig",
        "tags": ["Signature Configs"],
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "force",
            "in": "query",
            "schema": { "type": "boolean" },
            "description": "Overwrite existing config for the same provider"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["signature_config"],
                "properties": {
                  "signature_config": {
                    "type": "object",
                    "required": ["provider", "secret"],
                    "properties": {
                      "provider": {
                        "type": "string",
                        "enum": ["github", "stripe", "shopify", "slack", "twilio", "generic_hmac"]
                      },
                      "secret": {
                        "type": "string",
                        "description": "The webhook signing secret"
                      },
                      "enabled": {
                        "type": "boolean",
                        "default": true
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Signature config created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/SignatureConfig" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": {
            "description": "Config for this provider already exists. Use ?force=true to overwrite.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/endpoints/{endpoint_id}/actions": {
      "get": {
        "summary": "List endpoint actions",
        "description": "Returns the ordered Endpoint Actions configured for an endpoint.",
        "operationId": "listEndpointActions",
        "tags": ["Endpoint Actions"],
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "List of Endpoint Actions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/EndpointAction" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "post": {
        "summary": "Create an endpoint action",
        "description": "Creates an ordered action containing Forward, Notify, Tag, or Ignore steps. Forward steps require an active paid subscription.",
        "operationId": "createEndpointAction",
        "tags": ["Endpoint Actions"],
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["endpoint_action"],
                "properties": {
                  "endpoint_action": { "$ref": "#/components/schemas/EndpointActionInput" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Endpoint Action created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/EndpointAction" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/endpoints/{endpoint_id}/actions/{id}": {
      "get": {
        "summary": "Get an endpoint action",
        "description": "Returns the action configuration and its 10 most recent execution traces.",
        "operationId": "getEndpointAction",
        "tags": ["Endpoint Actions"],
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Endpoint Action details and recent runs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "allOf": [
                        { "$ref": "#/components/schemas/EndpointAction" },
                        {
                          "type": "object",
                          "properties": {
                            "recent_runs": {
                              "type": "array",
                              "items": { "$ref": "#/components/schemas/ActionRun" }
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "patch": {
        "summary": "Update an endpoint action",
        "description": "Updates an action. Accounts without active paid access may keep, remove, or convert a retained Forward step, but cannot change its Forward configuration.",
        "operationId": "updateEndpointAction",
        "tags": ["Endpoint Actions"],
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["endpoint_action"],
                "properties": {
                  "endpoint_action": { "$ref": "#/components/schemas/EndpointActionInput" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Endpoint Action updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "$ref": "#/components/schemas/EndpointAction" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      },
      "delete": {
        "summary": "Delete an endpoint action",
        "description": "Permanently deletes an Endpoint Action. Requires write scope.",
        "operationId": "deleteEndpointAction",
        "tags": ["Endpoint Actions"],
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "Endpoint Action deleted" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/events": {
      "get": {
        "summary": "List recent events",
        "description": "Returns recent activity events for the account. Requires read scope.",
        "operationId": "listEvents",
        "tags": ["Events"],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 },
            "description": "Number of events to return"
          },
          {
            "name": "since",
            "in": "query",
            "schema": { "type": "string", "format": "date-time" },
            "description": "Only return events after this ISO 8601 timestamp (default: 24 hours ago)"
          }
        ],
        "responses": {
          "200": {
            "description": "List of events",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Event" }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "count": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/tunnel/connect": {
      "post": {
        "summary": "Connect tunnel (authenticated)",
        "description": "Exchanges your API token for a one-time WebSocket ticket valid for 30 seconds. Requires tunnel scope.",
        "operationId": "tunnelConnect",
        "tags": ["Tunnel"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["endpoint_id"],
                "properties": {
                  "endpoint_id": {
                    "type": "string",
                    "description": "Endpoint to tunnel (e.g., ep_abc123)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "WebSocket ticket issued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": { "type": "string" },
                    "expires_in": { "type": "integer", "description": "Seconds until ticket expires" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/tunnel/connect_anonymous": {
      "post": {
        "summary": "Connect tunnel (anonymous)",
        "description": "Connects a tunnel to a temporary endpoint using a tunnel key. No authentication required.",
        "operationId": "tunnelConnectAnonymous",
        "tags": ["Tunnel"],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["tunnel_key"],
                "properties": {
                  "tunnel_key": {
                    "type": "string",
                    "description": "Tunnel key for the temporary endpoint (e.g., tk_xyz789)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "WebSocket ticket issued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": { "type": "string" },
                    "expires_in": { "type": "integer" },
                    "endpoint_id": { "type": "string" }
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/tunnel/undelivered": {
      "get": {
        "summary": "List undelivered tunnel webhooks",
        "description": "Returns captured webhooks that were not delivered through the selected authenticated tunnel endpoints. The legacy minutes form returns a data array only. New clients can use gap_id or since with keyset pagination and recovery metadata.",
        "operationId": "tunnelUndelivered",
        "tags": ["Tunnel"],
        "parameters": [
          {
            "name": "endpoint_ids[]",
            "in": "query",
            "required": true,
            "schema": { "type": "array", "items": { "type": "string" } },
            "description": "One or more authorized endpoint IDs"
          },
          {
            "name": "minutes",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 120, "default": 30 },
            "description": "Legacy short-window lookback"
          },
          {
            "name": "gap_id",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Durable tunnel gap ID to recover"
          },
          {
            "name": "since",
            "in": "query",
            "schema": { "type": "string", "format": "date-time" },
            "description": "Retention-bounded recovery start when no gap ID is supplied"
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Opaque keyset cursor from the previous page"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 100 }
          }
        ],
        "responses": {
          "200": {
            "description": "Undelivered webhooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/TunnelReplayRequest" } },
                    "meta": {
                      "type": "object",
                      "description": "Present only for gap-aware or since-based pagination",
                      "properties": {
                        "gap_id": { "type": ["string", "null"] },
                        "started_at": { "type": "string", "format": "date-time" },
                        "detected_at": { "type": ["string", "null"], "format": "date-time" },
                        "total_count": { "type": "integer" },
                        "pending_count": { "type": "integer" },
                        "returned_count": { "type": "integer" },
                        "next_cursor": { "type": ["string", "null"] },
                        "retention_truncated": { "type": "boolean" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/tunnel/gaps": {
      "get": {
        "summary": "List unresolved tunnel delivery gaps",
        "description": "Returns durable gap summaries for authorized endpoints without copying request payloads into the gap response.",
        "operationId": "tunnelGaps",
        "tags": ["Tunnel"],
        "parameters": [
          {
            "name": "endpoint_ids[]",
            "in": "query",
            "required": true,
            "schema": { "type": "array", "items": { "type": "string" } }
          }
        ],
        "responses": {
          "200": {
            "description": "Unresolved gaps ordered oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/TunnelGap" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/tunnel/gaps/{id}/recovery": {
      "post": {
        "summary": "Report tunnel gap recovery progress",
        "description": "Records a CLI recovery attempt. CatchHook derives final state from persisted tunnel delivery rows instead of trusting the submitted outcome alone.",
        "operationId": "tunnelGapRecovery",
        "tags": ["Tunnel"],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Tunnel gap ID (for example, tgap_abc123)"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["outcome", "attempted_count", "succeeded_count", "failed_count"],
                "properties": {
                  "outcome": { "type": "string", "enum": ["completed", "partial", "skipped", "interrupted"] },
                  "attempted_count": { "type": "integer", "minimum": 0 },
                  "succeeded_count": { "type": "integer", "minimum": 0 },
                  "failed_count": { "type": "integer", "minimum": 0 }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verified gap state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "data": { "$ref": "#/components/schemas/TunnelGap" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/tunnel/delivery_reports": {
      "post": {
        "summary": "Report tunnel delivery",
        "description": "Reports the result of a local tunnel delivery back to CatchHook for metrics and alerts. Requires tunnel scope.",
        "operationId": "tunnelDeliveryReport",
        "tags": ["Tunnel"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["endpoint_id", "webhook_request_id", "target_url", "status_code"],
                "properties": {
                  "endpoint_id": { "type": "string" },
                  "webhook_request_id": { "type": "string" },
                  "target_url": { "type": "string", "format": "uri" },
                  "status_code": { "type": "integer" },
                  "response_message": { "type": "string" },
                  "response_time_ms": { "type": "integer" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Delivery report recorded" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API token created in Account Settings. Include as: Authorization: Bearer <token>"
      }
    },
    "parameters": {
      "EndpointId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "Endpoint ID (e.g., ep_abc123)"
      }
    },
    "schemas": {
      "Endpoint": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "Prefixed ID (e.g., ep_abc123)" },
          "name": { "type": "string" },
          "url": { "type": "string", "format": "uri", "description": "Full webhook reception URL" },
          "provider": { "type": "string", "nullable": true, "description": "Provider preset if set" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "WebhookRequest": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "Prefixed ID (e.g., wr_def456)" },
          "endpoint_id": { "type": "string" },
          "method": { "type": "string", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] },
          "path": { "type": "string" },
          "headers": { "type": "object", "additionalProperties": { "type": "string" } },
          "body": { "type": "string", "nullable": true },
          "content_type": { "type": "string", "nullable": true },
          "source_ip": { "type": "string" },
          "query_params": { "type": "object", "additionalProperties": { "type": "string" } },
          "size": { "type": "integer", "description": "Body size in bytes" },
          "received_at": { "type": "string", "format": "date-time" }
        }
      },
      "ActionStepInput": {
        "type": "object",
        "required": ["step_type"],
        "properties": {
          "id": { "type": "string", "description": "Existing prefixed step ID when updating" },
          "step_type": { "type": "string", "enum": ["forward", "notify", "tag", "ignore"] },
          "name": { "type": "string", "nullable": true },
          "enabled": { "type": "boolean", "default": true },
          "position": { "type": "integer" },
          "failure_policy": { "type": "string", "enum": ["continue", "stop"], "nullable": true },
          "_destroy": { "type": "boolean", "description": "Remove this existing step when true" },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "description": "Step-specific configuration. Forward config includes url, timeout_seconds, retry_count, append_original_path, and append_original_query."
          }
        }
      },
      "EndpointActionInput": {
        "type": "object",
        "description": "name is required for POST (create), optional for PATCH (update).",
        "properties": {
          "name": { "type": "string" },
          "description": { "type": "string", "nullable": true },
          "enabled": { "type": "boolean", "default": true },
          "failure_policy": { "type": "string", "enum": ["continue", "stop"], "default": "continue" },
          "trigger_conditions": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["field", "operator"],
              "properties": {
                "field": { "type": "string" },
                "operator": { "type": "string" },
                "value": {}
              }
            }
          },
          "steps": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ActionStepInput" }
          }
        }
      },
      "ActionStep": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "Prefixed step ID (e.g., step_abc123)" },
          "step_type": { "type": "string", "enum": ["forward", "notify", "tag", "ignore"] },
          "name": { "type": "string", "nullable": true },
          "enabled": { "type": "boolean" },
          "position": { "type": "integer" },
          "config": { "type": "object", "additionalProperties": true },
          "failure_policy": { "type": "string", "enum": ["continue", "stop"], "nullable": true }
        }
      },
      "ActionStepRun": {
        "type": "object",
        "properties": {
          "step_type": { "type": "string" },
          "step_name": { "type": "string", "nullable": true },
          "status": { "type": "string" },
          "duration_ms": { "type": "integer", "nullable": true },
          "error_message": { "type": "string", "nullable": true },
          "output": { "type": "object", "additionalProperties": true, "nullable": true }
        }
      },
      "ActionRun": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "Prefixed action run ID" },
          "status": { "type": "string" },
          "trigger": { "type": "string" },
          "duration_ms": { "type": "integer", "nullable": true },
          "steps_executed": { "type": "integer" },
          "steps_failed": { "type": "integer" },
          "steps_skipped": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" },
          "step_runs": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ActionStepRun" }
          }
        }
      },
      "EndpointAction": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "Prefixed action ID (e.g., act_abc123)" },
          "name": { "type": "string" },
          "description": { "type": "string", "nullable": true },
          "enabled": { "type": "boolean" },
          "position": { "type": "integer" },
          "failure_policy": { "type": "string", "enum": ["continue", "stop"] },
          "trigger_conditions": { "type": "array", "items": { "type": "object" } },
          "steps": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ActionStep" }
          },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "SignatureConfig": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "provider": { "type": "string", "enum": ["github", "stripe", "shopify", "slack", "twilio", "generic_hmac"] },
          "enabled": { "type": "boolean" }
        }
      },
      "Event": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": ["webhook.received", "email.received", "forwarding.failed", "alert.triggered"]
          },
          "occurred_at": { "type": "string", "format": "date-time" },
          "endpoint_id": { "type": "string" },
          "request_id": { "type": "string", "nullable": true },
          "data": { "type": "object" }
        }
      },
      "TunnelGap": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "Prefixed gap ID (for example, tgap_abc123)" },
          "endpoint_id": { "type": "string" },
          "endpoint_name": { "type": "string" },
          "workspace_id": { "type": ["string", "null"] },
          "status": { "type": "string", "enum": ["open", "reconnected", "recovered", "partial", "skipped", "dismissed"] },
          "started_at": { "type": "string", "format": "date-time" },
          "detected_at": { "type": "string", "format": "date-time" },
          "reconnected_at": { "type": ["string", "null"], "format": "date-time" },
          "target_url": { "type": ["string", "null"] },
          "client_hostname": { "type": ["string", "null"] },
          "recovery_outcome": { "type": ["string", "null"] },
          "retention_truncated": { "type": "boolean" },
          "counts": {
            "type": "object",
            "properties": {
              "total_count": { "type": "integer" },
              "retained_count": { "type": "integer" },
              "pending_count": { "type": "integer" },
              "recovered_count": { "type": "integer" },
              "failed_count": { "type": "integer" },
              "expired_count": { "type": "integer" }
            }
          }
        }
      },
      "TunnelReplayRequest": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "const": "webhook_request" },
          "id": { "type": "string" },
          "endpoint_id": { "type": "string" },
          "method": { "type": "string" },
          "path": { "type": "string" },
          "headers": { "type": "object", "additionalProperties": true },
          "body": { "type": ["string", "null"], "description": "Base64-encoded request body" },
          "body_encoding": { "type": "string", "const": "base64" },
          "body_size": { "type": "integer" },
          "query_parameters": { "type": "object", "additionalProperties": true },
          "content_type": { "type": ["string", "null"] },
          "ip_address": { "type": ["string", "null"] },
          "requested_at": { "type": "string", "format": "date-time" },
          "detected_provider": { "type": ["string", "null"] },
          "provider_event_data": { "type": ["object", "null"], "additionalProperties": true }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Invalid or missing API token",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "Forbidden": {
        "description": "Token doesn't have the required scope",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "ValidationError": {
        "description": "Validation error",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limited — too many requests",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": { "type": "string" },
                "retry_after": { "type": "integer", "description": "Seconds to wait before retrying" }
              }
            }
          }
        }
      }
    }
  }
}
