Skip to content

Submit feedback · Feedback in aggregate

POST
/api/feedback

This address answers POST and QUERY, and both are sent as POST. The x-http-method-override header is what says which — see the sections below, and the parameter for the exact spelling.

POST — Submit feedback

Records one piece of feedback about this site or its tools, and confirms what was recorded. A submission is not served back at any address. A retry carrying the same Idempotency-Key confirms the same submission rather than recording a second one.

QUERY — Feedback in aggregate

What the submissions add up to — counts by kind and severity, and the subjects named most often. Written by a scheduled run rather than computed per request, so it lags the list above.

Header Parameters

idempotency-key?string

A value you choose, unique to this submission. It is what lets a retry after a timeout confirm the same submission rather than recording a second one. A quoted spelling is unwrapped; an empty value is refused.

Length1 <= length
x-http-method-override?"QUERY"

QUERY is sent as POST carrying x-http-method-override: QUERY, because the edge in front of this API refuses a bare QUERY with a 405 before any code here runs. Send it to reach the QUERY operation; leave it off to reach the POST one. It is the only thing distinguishing the two.

Value in

  • "QUERY"

Request Body

application/json

One of two shapes, chosen by the method-override header above.

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/problem+json

curl -X POST "https://example.com/api/feedback" \  -H "Content-Type: application/json" \  -d '{    "kind": "missing",    "severity": "high",    "message": "string"  }'
{  "aggregate": {    "total": 0,    "partial": true,    "cells": [      {        "kind": "missing",        "severity": "high",        "surface": "api",        "count": 0      }    ],    "about": [      {        "about": "string",        "count": 0      }    ]  }}
---
title: "Submit feedback · Feedback in aggregate"
description: "This address answers `POST` and `QUERY`, and both are sent as `POST`. The `x-http-method-override` header is what says which — see the sections below, and the parameter for the exact spelling.\n\n### `POST` — Submit feedback\n\nRecords one piece of feedback about this site or its tools, and confirms what was recorded. A submission is not served back at any address. A retry carrying the same `Idempotency-Key` confirms the same submission rather than recording a second one.\n\n### `QUERY` — Feedback in aggregate\n\nWhat the submissions add up to — counts by kind and severity, and the subjects named most often. Written by a scheduled run rather than computed per request, so it lags the list above."
url: "https://lateano.com/developers/api/reference/feedback/feedback.post"
author: "Colin Lateano"
---

# Submit feedback · Feedback in aggregate

This address answers `POST` and `QUERY`, and both are sent as `POST`. The `x-http-method-override` header is what says which — see the sections below, and the parameter for the exact spelling.

### `POST` — Submit feedback

Records one piece of feedback about this site or its tools, and confirms what was recorded. A submission is not served back at any address. A retry carrying the same `Idempotency-Key` confirms the same submission rather than recording a second one.

### `QUERY` — Feedback in aggregate

What the submissions add up to — counts by kind and severity, and the subjects named most often. Written by a scheduled run rather than computed per request, so it lags the list above.

```json
{
  "openapi": "3.1.1",
  "info": {
    "title": "lateano.com",
    "version": "1.0.0"
  },
  "paths": {
    "/api/feedback": {
      "post": {
        "operationId": "feedback.post",
        "summary": "Submit feedback · Feedback in aggregate",
        "tags": [
          "Feedback"
        ],
        "description": "This address answers `POST` and `QUERY`, and both are sent as `POST`. The `x-http-method-override` header is what says which — see the sections below, and the parameter for the exact spelling.\n\n### `POST` — Submit feedback\n\nRecords one piece of feedback about this site or its tools, and confirms what was recorded. A submission is not served back at any address. A retry carrying the same `Idempotency-Key` confirms the same submission rather than recording a second one.\n\n### `QUERY` — Feedback in aggregate\n\nWhat the submissions add up to — counts by kind and severity, and the subjects named most often. Written by a scheduled run rather than computed per request, so it lags the list above.",
        "parameters": [
          {
            "name": "idempotency-key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "description": "A value you choose, unique to this submission. It is what lets a retry after a timeout confirm the same submission rather than recording a second one. A quoted spelling is unwrapped; an empty value is refused."
          },
          {
            "name": "x-http-method-override",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "QUERY"
              ]
            },
            "description": "`QUERY` is sent as `POST` carrying `x-http-method-override: QUERY`, because the edge in front of this API refuses a bare `QUERY` with a 405 before any code here runs. Send it to reach the `QUERY` operation; leave it off to reach the `POST` one. It is the only thing distinguishing the two."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "One of two shapes, chosen by the method-override header above.",
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "title": "POST",
                    "type": "object",
                    "required": [
                      "kind",
                      "severity",
                      "message"
                    ],
                    "additionalProperties": false,
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "missing",
                          "broken",
                          "confusing",
                          "inaccurate",
                          "other"
                        ],
                        "description": "What the message is about: something that should be here and is not (missing), something here that does not work (broken), something here that is hard to follow (confusing), something here that is not true (inaccurate), or none of those (other)."
                      },
                      "severity": {
                        "type": "string",
                        "enum": [
                          "high",
                          "medium",
                          "low"
                        ],
                        "description": "How much it matters."
                      },
                      "message": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 2000,
                        "description": "The feedback itself."
                      },
                      "about": {
                        "type": "string",
                        "maxLength": 128,
                        "description": "A tool name or a page on this site the feedback is about."
                      }
                    }
                  },
                  {
                    "title": "QUERY",
                    "type": "object",
                    "additionalProperties": false,
                    "maxProperties": 0,
                    "description": "An empty object. This aggregate takes no parameters — a key sent here is refused rather than ignored, because a key accepted and ignored would answer a question next to the one asked."
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feedback in aggregate",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AggregateBody"
                }
              }
            }
          },
          "201": {
            "description": "Submit feedback",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmissionBody"
                }
              }
            }
          },
          "303": {
            "description": "The request accepted `text/html` — a browser form post. The submission is recorded and the browser is sent on with a `GET`. No body.",
            "headers": {
              "location": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "default": {
            "description": "Every other status this operation answers with is a refusal, and carries `problem+json` (RFC 9457).\n\nEvery refusal this API can make, whatever the status. Match on the `type` URI’s last segment — the slug below — rather than on the whole URI, which follows whichever deployment answered.\n\n| Status | Slug | Title |\n| --- | --- | --- |\n| 400 | [`invalid-idempotency-key`](https://lateano.com/api/problems/invalid-idempotency-key) | The Idempotency-Key header carries a value this resource will not accept |\n| 400 | [`malformed-body`](https://lateano.com/api/problems/malformed-body) | The request body did not parse |\n| 400 | [`missing-idempotency-key`](https://lateano.com/api/problems/missing-idempotency-key) | This resource requires an Idempotency-Key header |\n| 400 | [`missing-media-type`](https://lateano.com/api/problems/missing-media-type) | The request carried a body with no media type |\n| 404 | [`no-such-resource`](https://lateano.com/api/problems/no-such-resource) | There is nothing at this address |\n| 405 | [`method-not-allowed`](https://lateano.com/api/problems/method-not-allowed) | This resource does not answer that method |\n| 406 | [`not-acceptable`](https://lateano.com/api/problems/not-acceptable) | This resource cannot answer in any media type the request accepts |\n| 409 | [`conflicting-idempotency-key`](https://lateano.com/api/problems/conflicting-idempotency-key) | That Idempotency-Key already names a different submission |\n| 412 | [`precondition-failed`](https://lateano.com/api/problems/precondition-failed) | The condition the request carried does not hold |\n| 415 | [`unsupported-media-type`](https://lateano.com/api/problems/unsupported-media-type) | The request body is in a media type this resource does not read |\n| 422 | [`invalid-query`](https://lateano.com/api/problems/invalid-query) | The query parsed but does not describe a question this resource can answer |\n| 422 | [`invalid-submission`](https://lateano.com/api/problems/invalid-submission) | The submission parsed but is not something this resource can record |\n| 429 | [`too-many-requests`](https://lateano.com/api/problems/too-many-requests) | The courtesy quota for this caller is spent |\n| 500 | [`unexpected-failure`](https://lateano.com/api/problems/unexpected-failure) | The resource failed while answering |",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDocument"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AggregateBody": {
        "type": "object",
        "properties": {
          "aggregate": {
            "$ref": "#/components/schemas/Aggregate"
          }
        },
        "required": [
          "aggregate"
        ],
        "additionalProperties": false
      },
      "SubmissionBody": {
        "type": "object",
        "properties": {
          "about": {
            "type": [
              "string",
              "null"
            ]
          },
          "receivedAt": {
            "type": "string"
          }
        },
        "required": [
          "about",
          "receivedAt"
        ],
        "additionalProperties": false,
        "description": "The confirmation a submitter receives. `about` carries the resolved value rather than the value the caller sent, so the reply states what the message was filed against. The type comes from `Feedback`, so the two spellings cannot come apart."
      },
      "ProblemDocument": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "§ 3.1.1. Absolute, on the origin that answered, and it resolves."
          },
          "title": {
            "type": "string",
            "description": "§ 3.1.3. The catalogue row's, unchanged from occurrence to occurrence."
          },
          "status": {
            "type": "number",
            "description": "§ 3.1.2. The same number as the status line, derived rather than passed."
          },
          "instance": {
            "type": "string",
            "description": "§ 3.1.5 RECOMMENDS an absolute URI here. This field carries a full path from the root, which is the fallback form the same section permits."
          },
          "detail": {
            "type": "string",
            "description": "§ 3.1.4. This occurrence only, bounded by `DETAIL_MAX`, absent when there is nothing to add."
          }
        },
        "required": [
          "type",
          "title",
          "status",
          "instance"
        ],
        "additionalProperties": false,
        "description": "The schema generator publishes this declaration as the error schema, so a field here reaches the reference. RFC 9457 § 3.1 makes all five members optional, and this API narrows four of them to required: § 3.1.1 reads an absent `type` as `about:blank` and discards the resolvable URI. An optional member is absent, never present and `undefined`, because `JSON.stringify` drops the value and the wire cannot tell the two apart."
      },
      "Aggregate": {
        "type": "object",
        "properties": {
          "total": {
            "type": "number"
          },
          "partial": {
            "type": "boolean",
            "description": "`true` where one `list` did not reach the end. Pathnames are `sha256(key)` and `list` returns lexicographic order, so a partial subset is arbitrary with respect to time — \"the newest N\" can miss the actual newest."
          },
          "cells": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AggregateCell"
            },
            "description": "The cross, sparse and ranked. Cells with no submissions are absent, not zero."
          },
          "about": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AboutCount"
            },
            "description": "Resolved values only — Tool names and site paths, already ours and already published. Nothing a caller wrote reaches this list; the unresolved tail counts toward the unclassified bucket as a number, not a string."
          }
        },
        "required": [
          "total",
          "partial",
          "cells",
          "about"
        ],
        "additionalProperties": false
      },
      "AggregateCell": {
        "type": "object",
        "properties": {
          "kind": {
            "$ref": "#/components/schemas/Kind"
          },
          "severity": {
            "$ref": "#/components/schemas/Severity"
          },
          "surface": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Surface"
              },
              {
                "type": "null"
              }
            ],
            "description": "`null` is the unclassified bucket, a cell like any other; its size is the only measurement of how often `about` arrives at all."
          },
          "count": {
            "type": "number"
          }
        },
        "required": [
          "kind",
          "severity",
          "surface",
          "count"
        ],
        "additionalProperties": false,
        "description": "One cell of the `kind × severity × surface` cross."
      },
      "AboutCount": {
        "type": "object",
        "properties": {
          "about": {
            "type": "string"
          },
          "count": {
            "type": "number"
          }
        },
        "required": [
          "about",
          "count"
        ],
        "additionalProperties": false,
        "description": "One ranked `about` value."
      },
      "Kind": {
        "type": "string",
        "enum": [
          "missing",
          "broken",
          "confusing",
          "inaccurate",
          "other"
        ]
      },
      "Severity": {
        "type": "string",
        "enum": [
          "high",
          "medium",
          "low"
        ]
      },
      "Surface": {
        "type": "string",
        "enum": [
          "api",
          "content",
          "site",
          "demo"
        ]
      }
    }
  }
}
```