{
  "openapi": "3.1.0",
  "info": {
    "title": "Grapeworks public demo API",
    "version": "v1",
    "summary": "Two read-only endpoints serving the real Grapeworks warehouse schema over fabricated demo-tenant data.",
    "description": "A demo surface with the real schema and demo numbers. It is not the Grapeworks warehouse opened up.\n\nEvery byte these two endpoints return comes from static JSON checked into the website repository at build time, for `lochend-wild`, a fabricated demo tenant that is not a real business. There is no warehouse connection behind them, no gold view is read, no credential is used and no network call is made while a request is served. No figure returned here is a client result or a live measurement, and none of it should be quoted as one.\n\nBecause of that, every response body on every path, success and error alike, carries `demo: true` and `data_source: \"demo_fixture\"`, together with the tenant and a plain-English notice. The same statement is repeated in an `x-grapeworks-data-source: demo_fixture` response header for callers that only read headers.\n\nThese two endpoints are the whole public surface. No public endpoint returns live client data and there will not be one. The internal warehouse MCP server is stdio-only, has no network listener, and is not callable from the internet by any route.\n\nThis document is hand-written and describes exactly the two endpoints below. It describes nothing planned and nothing internal.",
    "contact": {
      "name": "Grapeworks",
      "email": "hello@grapeworks.ai",
      "url": "https://grapeworks.ai"
    },
    "license": {
      "name": "Demo data, free to read and quote as demo data",
      "identifier": "CC0-1.0"
    }
  },
  "externalDocs": {
    "description": "What an agent can verify about Grapeworks, and how",
    "url": "https://grapeworks.ai/agents"
  },
  "servers": [
    {
      "url": "https://grapeworks.ai",
      "description": "Production. Serves fixture data only on these two paths."
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "demo",
      "description": "The public demo surface. Fixture-backed, unauthenticated, rate-limited, read-only."
    }
  ],
  "paths": {
    "/api/demo/catalog": {
      "get": {
        "tags": [
          "demo"
        ],
        "operationId": "getDemoCatalog",
        "summary": "What is askable on the demo surface",
        "description": "Returns the metrics this demo surface will answer questions about: metric ids, their dimensions, their grain, the named windows they accept and the value fields they return. Read this before calling `/api/demo/query`.\n\nDemo-data caveat: the vocabulary is the real Grapeworks catalogue vocabulary, and the numbers reachable through it are fabricated demo-tenant fixtures. Nothing here reads a warehouse. The response carries `demo: true` and `data_source: \"demo_fixture\"`.\n\nUnknown parameters are rejected rather than ignored, and every rejection names the offending parameter and the accepted set.",
        "parameters": [
          {
            "name": "metric",
            "in": "query",
            "required": false,
            "description": "Narrows `askable` to one entry. Omit to list all four.",
            "schema": {
              "$ref": "#/components/schemas/MetricId"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The catalogue, optionally narrowed to one metric.",
            "headers": {
              "x-grapeworks-data-source": {
                "$ref": "#/components/headers/DataSource"
              },
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "options": {
        "tags": [
          "demo"
        ],
        "summary": "CORS preflight",
        "description": "Returns 204 with `Allow: GET, OPTIONS`. No body.",
        "responses": {
          "204": {
            "description": "No content.",
            "headers": {
              "allow": {
                "description": "Always `GET, OPTIONS`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "preflightDemoCatalog"
      },
      "post": {
        "tags": [
          "demo"
        ],
        "summary": "Not allowed",
        "description": "This surface is read-only. Any write method returns 405 with the standard error envelope, which still carries `demo: true` and `data_source: \"demo_fixture\"`.",
        "responses": {
          "405": {
            "description": "Read-only surface. Use GET.",
            "headers": {
              "allow": {
                "description": "Always `GET, OPTIONS`.",
                "schema": {
                  "type": "string"
                }
              },
              "x-grapeworks-data-source": {
                "$ref": "#/components/headers/DataSource"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "operationId": "postDemoCatalogNotAllowed"
      },
      "put": {
        "tags": [
          "demo"
        ],
        "summary": "Not allowed",
        "description": "This surface is read-only. Any write method returns 405 with the standard error envelope, which still carries `demo: true` and `data_source: \"demo_fixture\"`.",
        "responses": {
          "405": {
            "description": "Read-only surface. Use GET.",
            "headers": {
              "allow": {
                "description": "Always `GET, OPTIONS`.",
                "schema": {
                  "type": "string"
                }
              },
              "x-grapeworks-data-source": {
                "$ref": "#/components/headers/DataSource"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "operationId": "putDemoCatalogNotAllowed"
      },
      "patch": {
        "tags": [
          "demo"
        ],
        "summary": "Not allowed",
        "description": "This surface is read-only. Any write method returns 405 with the standard error envelope, which still carries `demo: true` and `data_source: \"demo_fixture\"`.",
        "responses": {
          "405": {
            "description": "Read-only surface. Use GET.",
            "headers": {
              "allow": {
                "description": "Always `GET, OPTIONS`.",
                "schema": {
                  "type": "string"
                }
              },
              "x-grapeworks-data-source": {
                "$ref": "#/components/headers/DataSource"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "operationId": "patchDemoCatalogNotAllowed"
      },
      "delete": {
        "tags": [
          "demo"
        ],
        "summary": "Not allowed",
        "description": "This surface is read-only. Any write method returns 405 with the standard error envelope, which still carries `demo: true` and `data_source: \"demo_fixture\"`.",
        "responses": {
          "405": {
            "description": "Read-only surface. Use GET.",
            "headers": {
              "allow": {
                "description": "Always `GET, OPTIONS`.",
                "schema": {
                  "type": "string"
                }
              },
              "x-grapeworks-data-source": {
                "$ref": "#/components/headers/DataSource"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "operationId": "deleteDemoCatalogNotAllowed"
      }
    },
    "/api/demo/query": {
      "get": {
        "tags": [
          "demo"
        ],
        "operationId": "getDemoQuery",
        "summary": "A structured, schema-correct answer with provenance",
        "description": "Returns rows for one metric, optionally grouped by one of its dimensions, over one of its named windows, with a provenance block.\n\nDemo-data caveat: every row is fabricated data for the demo tenant `lochend-wild`, served from static JSON vendored into the website repository. No warehouse query is executed, no gold view is read and no credential is used. `provenance.goldViews` is therefore always an empty array, and the response carries `demo: true` and `data_source: \"demo_fixture\"`. These numbers are not a client result.\n\nArbitrary date ranges are not supported. Use one of the named windows the catalogue lists for the metric. There is no tenant selector: this surface serves exactly one fabricated tenant and rejects any attempt to point it elsewhere.",
        "parameters": [
          {
            "name": "metric",
            "in": "query",
            "required": true,
            "description": "Which metric to return. Required.",
            "schema": {
              "$ref": "#/components/schemas/MetricId"
            }
          },
          {
            "name": "dimension",
            "in": "query",
            "required": false,
            "description": "Group by one of the metric's own dimensions. Omit for the ungrouped series. Supplying a dimension a metric does not have is a 400, and `true_cac` has no dimensions at all.",
            "schema": {
              "type": "string",
              "enum": [
                "campaign_id",
                "publisher_platform",
                "platform_position",
                "channel"
              ]
            }
          },
          {
            "name": "window",
            "in": "query",
            "required": false,
            "description": "One of the named windows the catalogue lists for that metric. Defaults to the first window in the metric's `windows` array: `last_7_days` for `spend_pacing`, `last_28_days` for `placement_distribution`, `last_3_months` for `revenue_by_channel` and `true_cac`.",
            "schema": {
              "type": "string",
              "enum": [
                "last_7_days",
                "last_28_days",
                "last_3_months",
                "last_12_months"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return. A hard ceiling of 50 rows applies whatever is asked for.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rows for the requested metric, with provenance.",
            "headers": {
              "x-grapeworks-data-source": {
                "$ref": "#/components/headers/DataSource"
              },
              "x-ratelimit-limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "x-ratelimit-remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "x-ratelimit-reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueryResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "options": {
        "tags": [
          "demo"
        ],
        "summary": "CORS preflight",
        "description": "Returns 204 with `Allow: GET, OPTIONS`. No body.",
        "responses": {
          "204": {
            "description": "No content.",
            "headers": {
              "allow": {
                "description": "Always `GET, OPTIONS`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "preflightDemoQuery"
      },
      "post": {
        "tags": [
          "demo"
        ],
        "summary": "Not allowed",
        "description": "This surface is read-only. Any write method returns 405 with the standard error envelope, which still carries `demo: true` and `data_source: \"demo_fixture\"`.",
        "responses": {
          "405": {
            "description": "Read-only surface. Use GET.",
            "headers": {
              "allow": {
                "description": "Always `GET, OPTIONS`.",
                "schema": {
                  "type": "string"
                }
              },
              "x-grapeworks-data-source": {
                "$ref": "#/components/headers/DataSource"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "operationId": "postDemoQueryNotAllowed"
      },
      "put": {
        "tags": [
          "demo"
        ],
        "summary": "Not allowed",
        "description": "This surface is read-only. Any write method returns 405 with the standard error envelope, which still carries `demo: true` and `data_source: \"demo_fixture\"`.",
        "responses": {
          "405": {
            "description": "Read-only surface. Use GET.",
            "headers": {
              "allow": {
                "description": "Always `GET, OPTIONS`.",
                "schema": {
                  "type": "string"
                }
              },
              "x-grapeworks-data-source": {
                "$ref": "#/components/headers/DataSource"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "operationId": "putDemoQueryNotAllowed"
      },
      "patch": {
        "tags": [
          "demo"
        ],
        "summary": "Not allowed",
        "description": "This surface is read-only. Any write method returns 405 with the standard error envelope, which still carries `demo: true` and `data_source: \"demo_fixture\"`.",
        "responses": {
          "405": {
            "description": "Read-only surface. Use GET.",
            "headers": {
              "allow": {
                "description": "Always `GET, OPTIONS`.",
                "schema": {
                  "type": "string"
                }
              },
              "x-grapeworks-data-source": {
                "$ref": "#/components/headers/DataSource"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "operationId": "patchDemoQueryNotAllowed"
      },
      "delete": {
        "tags": [
          "demo"
        ],
        "summary": "Not allowed",
        "description": "This surface is read-only. Any write method returns 405 with the standard error envelope, which still carries `demo: true` and `data_source: \"demo_fixture\"`.",
        "responses": {
          "405": {
            "description": "Read-only surface. Use GET.",
            "headers": {
              "allow": {
                "description": "Always `GET, OPTIONS`.",
                "schema": {
                  "type": "string"
                }
              },
              "x-grapeworks-data-source": {
                "$ref": "#/components/headers/DataSource"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "operationId": "deleteDemoQueryNotAllowed"
      }
    }
  },
  "components": {
    "headers": {
      "DataSource": {
        "description": "Always `demo_fixture`. Present on every response, including errors, so a caller that reads only headers still sees that the payload is fixture data.",
        "schema": {
          "type": "string",
          "const": "demo_fixture"
        }
      },
      "RateLimitLimit": {
        "description": "Requests allowed per window per IP, across both demo routes combined.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Unix time in seconds at which the current window resets.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was rejected. Causes: an unknown or repeated parameter (`request.malformed`); a tenant selector such as `client_id` or `tenant_id` (`request.client_id_in_body_forbidden`); a metric this surface does not answer (`spec.metric_not_askable`); or a parameter that is empty, out of range, or not valid for the named metric (`spec.invalid`). Every rejection names the offending parameter and the accepted set in `error.details`, so the interface can be learned from a failure as readily as from a success.",
        "headers": {
          "x-grapeworks-data-source": {
            "$ref": "#/components/headers/DataSource"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "unknownMetric": {
                "summary": "A metric this surface does not answer",
                "value": {
                  "apiVersion": "v1",
                  "demo": true,
                  "data_source": "demo_fixture",
                  "tenant": "lochend-wild",
                  "notice": "Demo surface: real Grapeworks warehouse schema, fabricated demo-tenant numbers. \u2026",
                  "error": {
                    "code": "spec.metric_not_askable",
                    "message": "'roas' is not askable on this demo surface.",
                    "retryable": false,
                    "details": {
                      "parameter": "metric",
                      "value": "roas",
                      "askable": [
                        "spend_pacing",
                        "placement_distribution",
                        "revenue_by_channel",
                        "true_cac"
                      ]
                    }
                  }
                }
              },
              "tenantSelector": {
                "summary": "An attempt to point the surface at another tenant",
                "value": {
                  "apiVersion": "v1",
                  "demo": true,
                  "data_source": "demo_fixture",
                  "tenant": "lochend-wild",
                  "notice": "Demo surface: real Grapeworks warehouse schema, fabricated demo-tenant numbers. \u2026",
                  "error": {
                    "code": "request.client_id_in_body_forbidden",
                    "message": "This surface takes no tenant selector. It serves exactly one fabricated demo tenant and cannot be pointed at anything else.",
                    "retryable": false,
                    "details": {
                      "parameter": "client_id",
                      "tenant": "lochend-wild"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Over the per-IP cap of 30 requests per 60 seconds, counted across both demo routes together. `retry-after` says how long to wait. The cap is enforced in the handler with an in-memory per-instance store, so under serverless fan-out the effective cap is per instance rather than global.",
        "headers": {
          "retry-after": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          },
          "x-grapeworks-data-source": {
            "$ref": "#/components/headers/DataSource"
          },
          "x-ratelimit-limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "x-ratelimit-remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "x-ratelimit-reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "InternalError": {
        "description": "A fixture combination the catalogue advertises is missing from the vendored data. That is a defect in the fixtures, not a limit on the caller. `retryable` is true.",
        "headers": {
          "x-grapeworks-data-source": {
            "$ref": "#/components/headers/DataSource"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    },
    "schemas": {
      "MetricId": {
        "type": "string",
        "description": "The four metrics this demo surface answers. Ids are lifted verbatim from the real Grapeworks catalogue, so an agent learns the true vocabulary.",
        "enum": [
          "spend_pacing",
          "placement_distribution",
          "revenue_by_channel",
          "true_cac"
        ]
      },
      "DemoEnvelope": {
        "type": "object",
        "description": "The labelling contract carried by every response body on every path, success and error alike.",
        "required": [
          "apiVersion",
          "demo",
          "data_source",
          "tenant",
          "notice"
        ],
        "properties": {
          "apiVersion": {
            "type": "string",
            "const": "v1"
          },
          "demo": {
            "type": "boolean",
            "const": true,
            "description": "Always true. This surface has no non-demo mode."
          },
          "data_source": {
            "type": "string",
            "const": "demo_fixture",
            "description": "Always `demo_fixture`. Static JSON vendored into the website repository at build time."
          },
          "tenant": {
            "type": "string",
            "const": "lochend-wild",
            "description": "The fabricated demo tenant. Not a real business."
          },
          "notice": {
            "type": "string",
            "description": "Plain-English restatement of the demo-data caveat, so a caller that reads only the body still gets it in words."
          }
        }
      },
      "AskableMetric": {
        "type": "object",
        "required": [
          "id",
          "label",
          "description",
          "dimensions",
          "grain",
          "windows",
          "value_fields"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/MetricId"
          },
          "label": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "dimensions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Dimensions this metric may be grouped by. Empty means the metric can only be returned ungrouped."
          },
          "grain": {
            "type": [
              "string",
              "null"
            ],
            "description": "Time grain of the rows, or null where the metric has no time axis.",
            "examples": [
              "day",
              "month",
              null
            ]
          },
          "windows": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The named windows accepted for this metric. The first entry is the default. Arbitrary ranges are not accepted."
          },
          "value_fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The numeric fields each row carries."
          }
        }
      },
      "Limits": {
        "type": "object",
        "description": "The published caps, so a caller can plan rather than discover them by being refused.",
        "properties": {
          "max_rows": {
            "type": "integer"
          },
          "rate_limit_requests": {
            "type": "integer"
          },
          "rate_limit_window_seconds": {
            "type": "integer"
          },
          "scope": {
            "type": "string"
          }
        }
      },
      "CatalogResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DemoEnvelope"
          },
          {
            "type": "object",
            "required": [
              "as_of",
              "tenant_note",
              "askable",
              "filters",
              "limits",
              "query_endpoint",
              "docs"
            ],
            "properties": {
              "as_of": {
                "type": "string",
                "format": "date",
                "description": "The fixed date the fixture data is stated as of. It does not move, so worked examples stay true."
              },
              "tenant_note": {
                "type": "string",
                "description": "States in words that the tenant is fabricated."
              },
              "askable": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AskableMetric"
                },
                "description": "All four metrics, or the single one named by the `metric` parameter."
              },
              "filters": {
                "type": "object",
                "description": "What can and cannot be filtered on, stated rather than left to be discovered.",
                "properties": {
                  "supported": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "not_supported": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "limits": {
                "$ref": "#/components/schemas/Limits"
              },
              "query_endpoint": {
                "type": "string",
                "const": "/api/demo/query"
              },
              "docs": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        ]
      },
      "QueryRow": {
        "type": "object",
        "description": "One row. Keys are the metric's grain field where it has one (`date` or `month`), the requested dimension where one was requested, and the metric's value fields.",
        "additionalProperties": true,
        "examples": [
          {
            "date": "2026-08-04",
            "campaign_id": "lw_brand_always_on",
            "spend_gbp": 60.18,
            "cumulative_spend_gbp": 60.18
          },
          {
            "month": "2026-06",
            "channel": "paid_social",
            "revenue_gbp": 454.08
          },
          {
            "publisher_platform": "instagram",
            "spend_gbp": 1505.3,
            "pixel_purchases": 38
          }
        ]
      },
      "Provenance": {
        "type": "object",
        "description": "Where the answer came from. On this surface the honest answer is: a static file. The camelCase keys mirror the internal provenance shape so the demo teaches the real one.",
        "required": [
          "metric",
          "goldViews",
          "asOf",
          "rowCount",
          "data_source",
          "fixture",
          "generated_from"
        ],
        "properties": {
          "metric": {
            "$ref": "#/components/schemas/MetricId"
          },
          "goldViews": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Always empty here, because no gold view was read.",
            "maxItems": 0
          },
          "asOf": {
            "type": "string",
            "format": "date"
          },
          "dataWindow": {
            "type": "object",
            "required": [
              "start",
              "end"
            ],
            "properties": {
              "start": {
                "type": "string",
                "format": "date"
              },
              "end": {
                "type": "string",
                "format": "date"
              }
            }
          },
          "rowCount": {
            "type": "integer"
          },
          "data_source": {
            "type": "string",
            "const": "demo_fixture"
          },
          "fixture": {
            "type": "string",
            "description": "Path of the checked-in file the rows were sliced from."
          },
          "generated_from": {
            "type": "string",
            "description": "States in a sentence that no warehouse query ran, no gold view was read and no credential was used."
          },
          "tenant_note": {
            "type": "string"
          }
        }
      },
      "QueryResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DemoEnvelope"
          },
          {
            "type": "object",
            "required": [
              "query",
              "rows",
              "rowCount",
              "truncated",
              "provenance",
              "limits"
            ],
            "properties": {
              "query": {
                "type": "object",
                "description": "The request as the endpoint resolved it, including any default it applied.",
                "required": [
                  "metric",
                  "dimension",
                  "window",
                  "grain",
                  "limit"
                ],
                "properties": {
                  "metric": {
                    "$ref": "#/components/schemas/MetricId"
                  },
                  "dimension": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Null when no dimension was requested."
                  },
                  "window": {
                    "type": "string"
                  },
                  "grain": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "limit": {
                    "type": "integer"
                  }
                }
              },
              "rows": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/QueryRow"
                },
                "maxItems": 50
              },
              "rowCount": {
                "type": "integer"
              },
              "truncated": {
                "type": "boolean",
                "description": "True when the fixture held more rows than were returned."
              },
              "provenance": {
                "$ref": "#/components/schemas/Provenance"
              },
              "limits": {
                "$ref": "#/components/schemas/Limits"
              }
            }
          }
        ]
      },
      "ErrorResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DemoEnvelope"
          },
          {
            "type": "object",
            "required": [
              "error"
            ],
            "properties": {
              "error": {
                "type": "object",
                "required": [
                  "code",
                  "message",
                  "retryable"
                ],
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "A closed taxonomy, a subset of the internal one. Branch on this, not on the message.",
                    "enum": [
                      "request.malformed",
                      "request.client_id_in_body_forbidden",
                      "spec.metric_not_askable",
                      "spec.invalid",
                      "rate.limited",
                      "request.method_not_allowed",
                      "internal"
                    ]
                  },
                  "message": {
                    "type": "string"
                  },
                  "retryable": {
                    "type": "boolean",
                    "description": "Derived from the code, never supplied by the caller. True for `rate.limited` and `internal`, false for the rest."
                  },
                  "details": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Names the offending parameter and the accepted set: `allowed`, `askable`, `minimum`, `maximum`, `value`."
                  }
                }
              }
            }
          }
        ]
      }
    }
  }
}
