{
  "openapi": "3.1.0",
  "info": {
    "title": "CommerceHook API",
    "description": "Manage webhook endpoints and read captured events programmatically. Authenticate with an API key created in the dashboard settings, or an OAuth 2.1 access token from a connected app, either sent as a bearer token. Requests are limited to 120 per minute per credential; 429 responses carry a Retry-After header.",
    "version": "2026.09.17",
    "contact": {
      "email": "help@commercehook.app"
    },
    "termsOfService": "https://commercehook.app/terms/"
  },
  "servers": [
    {
      "url": "https://app.commercehook.app"
    }
  ],
  "security": [
    {
      "apiKey": []
    },
    {
      "oauthToken": []
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "chk_ API key",
        "description": "Create a key at https://app.commercehook.app/settings. Keys grant full account access."
      },
      "oauthToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "OAuth 2.1 access token",
        "description": "An access token from the OAuth flow described at https://commercehook.app/auth.md works everywhere an API key does, with the same 120/min budget counted per connected app. Connected apps are visible and revocable in settings."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "Endpoint": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "createdAt": {
            "type": "integer",
            "description": "Epoch milliseconds"
          },
          "url": {
            "type": "string",
            "description": "The address to register as your webhook destination"
          },
          "eventCount": {
            "type": "integer"
          },
          "lastReceivedAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Epoch milliseconds of the newest event, null before the first delivery"
          }
        },
        "required": [
          "id",
          "name",
          "createdAt",
          "url",
          "eventCount"
        ]
      },
      "EventSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "eventType": {
            "type": [
              "string",
              "null"
            ]
          },
          "platform": {
            "type": [
              "string",
              "null"
            ],
            "description": "Detected sender, e.g. bigcommerce, shopify"
          },
          "method": {
            "type": [
              "string",
              "null"
            ]
          },
          "receivedAt": {
            "type": "integer",
            "description": "Epoch milliseconds"
          },
          "payloadPreview": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "eventType",
          "platform",
          "method",
          "receivedAt",
          "payloadPreview"
        ]
      },
      "EventDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "endpointId": {
            "type": "string"
          },
          "eventType": {
            "type": [
              "string",
              "null"
            ]
          },
          "platform": {
            "type": [
              "string",
              "null"
            ]
          },
          "method": {
            "type": [
              "string",
              "null"
            ]
          },
          "receivedAt": {
            "type": "integer"
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "The headers as stored: exactly as delivered, except that a credential header (Authorization, Cookie, Proxy-Authorization) carries the value [redacted] and the headers Cloudflare adds in transit (cf-*, x-forwarded-*, x-real-ip, true-client-ip) are not kept"
          },
          "query": {
            "type": [
              "string",
              "null"
            ],
            "description": "Query string exactly as delivered, without the leading '?', or null when there was none. Events captured before July 2026 have null regardless of what they arrived with"
          },
          "params": {
            "type": "array",
            "description": "The query string as [name, value] pairs, in the order delivered. A list rather than an object, because a name may legally repeat",
            "items": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "minItems": 2,
              "maxItems": 2
            }
          },
          "payload": {
            "description": "Parsed JSON payload, or null when the body was not valid JSON or was empty"
          },
          "payloadRaw": {
            "type": "string",
            "description": "The body exactly as delivered"
          },
          "replays": {
            "type": "array",
            "description": "Past replay attempts for this event, newest first, at most 20",
            "items": {
              "$ref": "#/components/schemas/ReplayAttempt"
            }
          },
          "signatureValid": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the delivery's platform signature (currently WooCommerce's X-WC-Webhook-Signature) verifies against the secret registered through CommerceHook. Null when there is nothing to check: no signature header, or no stored secret"
          }
        },
        "required": [
          "id",
          "endpointId",
          "receivedAt",
          "headers",
          "params",
          "payloadRaw",
          "replays",
          "signatureValid"
        ]
      },
      "ReplayAttempt": {
        "type": "object",
        "properties": {
          "destination": {
            "type": "string"
          },
          "status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The destination's HTTP status, or null when it could not be reached at all"
          },
          "ok": {
            "type": "boolean"
          },
          "durationMs": {
            "type": "integer"
          },
          "createdAt": {
            "type": "integer",
            "description": "Epoch milliseconds"
          },
          "replayedBy": {
            "type": [
              "string",
              "null"
            ],
            "description": "Email of whoever ran the replay, or null when that account no longer exists"
          },
          "formerMember": {
            "type": "boolean",
            "description": "True when the account that ran it has since been deleted"
          }
        },
        "required": [
          "destination",
          "status",
          "ok",
          "durationMs",
          "createdAt",
          "replayedBy",
          "formerMember"
        ]
      },
      "ReplayResult": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True when the destination returned 2xx"
          },
          "status": {
            "type": "integer"
          },
          "durationMs": {
            "type": "integer"
          },
          "responsePreview": {
            "type": "string",
            "description": "First 500 characters of the destination's response body"
          }
        },
        "required": [
          "ok",
          "status",
          "durationMs",
          "responsePreview"
        ]
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "last4": {
            "type": "string"
          },
          "createdAt": {
            "type": "integer"
          },
          "lastUsedAt": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "last4",
          "createdAt"
        ]
      }
    },
    "responses": {
      "Unauthorised": {
        "description": "Missing, invalid, or revoked credentials",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found, or not yours (existence is never confirmed)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded; retry after the number of seconds in the Retry-After header",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/me": {
      "get": {
        "summary": "Current account",
        "description": "Returns the account the credentials belong to; the simplest way to check a key works.",
        "operationId": "getMe",
        "responses": {
          "200": {
            "description": "The account",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string"
                    },
                    "plan": {
                      "type": "string",
                      "enum": [
                        "free",
                        "pro"
                      ]
                    },
                    "createdAt": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/endpoints": {
      "get": {
        "summary": "List endpoints",
        "operationId": "listEndpoints",
        "responses": {
          "200": {
            "description": "All endpoints on the account",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "endpoints": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Endpoint"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "summary": "Create an endpoint",
        "operationId": "createEndpoint",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new endpoint, including its webhook URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Endpoint"
                }
              }
            }
          },
          "400": {
            "description": "Invalid name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "403": {
            "description": "Free plan endpoint limit reached",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/endpoints/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "patch": {
        "summary": "Rename an endpoint",
        "operationId": "renameEndpoint",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Renamed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "summary": "Delete an endpoint and all its stored events",
        "operationId": "deleteEndpoint",
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/endpoints/{id}/export": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "summary": "Export the endpoint's stored events as a JSON or HAR file",
        "operationId": "exportEvents",
        "description": "Streams the endpoint's whole stored history, oldest first, as a downloadable file. format=json (the default) carries endpoint metadata and every event with headers, query string and raw body; format=har is a HAR 1.2 capture that opens in browser devtools. q applies the same free-text filter as the event list.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "har"
              ],
              "default": "json"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Free-text filter over event type and payload"
          }
        ],
        "responses": {
          "200": {
            "description": "The export file, streamed. Content-Disposition names it commercehook-{id}.json or .har.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Unknown format"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/events": {
      "get": {
        "summary": "List events for an endpoint, newest first",
        "operationId": "listEvents",
        "parameters": [
          {
            "name": "endpointId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque token from the previous response's nextCursor"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Free-text search over event type and payload"
          }
        ],
        "responses": {
          "200": {
            "description": "One page of events",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EventSummary"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/events/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "summary": "One event in full: payload, headers, metadata",
        "operationId": "getEvent",
        "responses": {
          "200": {
            "description": "The event",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/events/{id}/replay": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "summary": "Replay a stored event at a destination of your choosing",
        "description": "Re-sends the original payload with the original method and platform headers (transit and credential headers are stripped) to the given URL, plus an x-commercehook-replay header carrying the event id. The destination's status and response preview are reported back; a non-2xx destination response is not an error here.",
        "operationId": "replayEvent",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "http or https destination"
                  }
                },
                "required": [
                  "url"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery attempted; see ok and status for the outcome",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReplayResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid destination URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The destination could not be reached at all",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/keys": {
      "get": {
        "summary": "List API keys (metadata only)",
        "operationId": "listApiKeys",
        "responses": {
          "200": {
            "description": "Key metadata; full keys are never returned after creation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApiKey"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "summary": "Create an API key",
        "description": "The response contains the full key exactly once; store it securely.",
        "operationId": "createApiKey",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new key, shown this once",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ApiKey"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "key"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "403": {
            "description": "Key limit reached (10 per account)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/keys/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "delete": {
        "summary": "Revoke an API key immediately",
        "operationId": "revokeApiKey",
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorised"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  }
}
