{
  "openapi": "3.1.0",
  "info": {
    "title": "zpe-newsletter",
    "version": "1.1.0",
    "description": "A newsletter on zpe.vc/newsletter: one list per account, subscribers in that account's own D1, campaigns sent by the operator's own provider. Every operation lives under the one base /_/api/v1, and this document is served at /_/api/v1/openapi.json. BOTH OPERATIONS ARE THE OWNER'S: the credential is the door's session cookie (a signed-in owner's browser, or any client holding that cookie). This deployment mints no API keys and verifies no bearer token, so an anonymous call is refused 401, or 503 when the door cannot answer — never quietly served.\n\n## This surface reads, and writes\n\n`GET /_/api/v1/newsletter` answers this account's one list: its settings, the three tallies (`subscribed`, `pending`, `unsubscribed`), the recent subscribers and the recent campaigns. A deployment with no list for the account answers `200` with `list: null`, zero tallies and empty collections, which is the truth rather than a `404`: the account exists and holds nothing yet. No subscriber, confirm or unsubscribe token is published. Both writes below are unchanged."
  },
  "servers": [
    {
      "url": "/newsletter",
      "description": "zpe-newsletter on zpe.vc"
    }
  ],
  "paths": {
    "/_/api/v1/newsletter": {
      "get": {
        "operationId": "getNewsletter",
        "summary": "This account's one list as the console draws it: its settings, its tallies, its recent subscribers and its campaigns",
        "tags": [
          "newsletter"
        ],
        "responses": {
          "200": {
            "description": "The account's list — `null` when it has none, with the tallies at zero and both collections empty. `list` is the settings the console edits; `counts` is the three tallies (`subscribed`, `pending`, `unsubscribed`); `subscribers` is the most recent rows the console shows, without any of their tokens; `campaigns` is the recent sends.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "account",
                    "list",
                    "counts",
                    "subscribers",
                    "campaigns"
                  ],
                  "properties": {
                    "account": {
                      "type": "string"
                    },
                    "list": {
                      "type": "object",
                      "nullable": true
                    },
                    "counts": {
                      "type": "object",
                      "required": [
                        "subscribed",
                        "pending",
                        "unsubscribed"
                      ],
                      "properties": {
                        "subscribed": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "pending": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "unsubscribed": {
                          "type": "integer",
                          "minimum": 0
                        }
                      }
                    },
                    "subscribers": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "campaigns": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No session cookie and no key, so there is nobody whose list this could be.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential was real and does not open this operation: the door named no account, or the key does not carry this route's grant or scope.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "This key has spent its request ceiling for the moment; retry once the wait the answer names has passed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The door (auth.zpe.vc) did not answer, so the caller could not be named and nothing was read.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      },
      "post": {
        "operationId": "postNewsletter",
        "summary": "Create this account's one newsletter list, or change its settings",
        "tags": [
          "newsletter"
        ],
        "responses": {
          "200": {
            "description": "The account's list existed and its settings are now what this request asked for; no new list was created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "201": {
            "description": "The account had no list and this request created it, at the `slug` it named.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "The request named no usable list: the title, the from name, the from address, or the public address (1-40 characters of a-z, 0-9 and inner hyphens) is missing or malformed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No session cookie and no key, so there is nobody whose list this could be.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential was real and does not open this operation: the door named no account, or the key does not carry this route's grant or scope.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "That public address already belongs to another account's list; one slug exists once across the estate.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "This key has spent its request ceiling for the moment; retry once the wait the answer names has passed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The door (auth.zpe.vc) did not answer, so the caller could not be named and nothing was written.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/send": {
      "post": {
        "operationId": "postSend",
        "summary": "Queue a campaign to the list, or send one test to a single address",
        "tags": [
          "campaigns"
        ],
        "responses": {
          "200": {
            "description": "The campaign was queued for delivery, or — with `mode: test` — the one test email was sent to the address that was asked for.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Nothing was queued: no list exists yet, the subject or the HTML is empty, the test address is malformed, or this deployment has no sender wired (`EMAIL_API_URL`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No session cookie and no key, so there is nobody whose campaign this could be.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential was real and does not open this operation: the door named no account, or the key does not carry this route's grant or scope.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "This key has spent its request ceiling for the moment; retry once the wait the answer names has passed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The door (auth.zpe.vc) did not answer, so the caller could not be named and nothing was queued.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key minted on the account's settings page at zpe.vc/home/settings. Send it as `Authorization: Bearer <key>`."
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem+json: `type`, `title`, `status`, `code` and `instance`, plus `detail` when a person should read a sentence.",
        "required": [
          "type",
          "title",
          "status",
          "code",
          "instance"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "enum": [
              "bad-request",
              "unauthenticated",
              "malformed",
              "unknown",
              "mismatch",
              "revoked",
              "expired",
              "another-account",
              "scope-refused",
              "grant-refused",
              "forbidden",
              "not-found",
              "method-not-allowed",
              "conflict",
              "gone",
              "payload-too-large",
              "unsupported-media-type",
              "unprocessable",
              "rate-limited",
              "payment-required",
              "not-open",
              "internal",
              "unavailable"
            ]
          },
          "instance": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          }
        }
      }
    }
  },
  "x-surface": {
    "app": "zpe-newsletter",
    "base": "/_/api/v1",
    "document": "/_/api/v1/openapi.json",
    "generated_by": "zpe-kit machineDocument"
  },
  "x-zpe-routes": 3,
  "x-zpe-documented": 3,
  "x-zpe-internal": []
}
