{
  "openapi": "3.0.3",
  "info": {
    "title": "AO Job Heartbeat",
    "version": "0.1.0",
    "description": "Dead-man's-switch monitoring for scheduled jobs and agent runs. A check has a period and a grace; your job pings its URL each run. No ping within period + grace sends a signed check.late event to your email (every plan) and webhooks (paid plan); the next ping sends check.recovered. Plans: Free = $0, 10 checks, email alerts, 30-day event log. Paid = $7/month flat, 100 checks, email + webhooks, 30-day event log, no per-seat fee. Alerts are email and webhook only; there is no SMS or phone channel on any plan.",
    "contact": {"name": "Austin Oaks Solutions", "url": "https://beat.austinoaksapi.com/"}
  },
  "servers": [{"url": "https://beat.austinoaksapi.com"}],
  "tags": [
    {"name": "account", "description": "Signup, account view, billing"},
    {"name": "checks", "description": "Checks and their event log"},
    {"name": "targets", "description": "Where alerts go: webhook (paid) or your own email"},
    {"name": "ping", "description": "Called by the job itself. No authentication."},
    {"name": "verify", "description": "Public key for verifying signed events"},
    {"name": "ops", "description": "Health and operator metrics"}
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {"type": "http", "scheme": "bearer", "description": "Authorization: Bearer ak_... (the key from POST /v1/signup). X-API-Key is also accepted."},
      "adminToken": {"type": "apiKey", "in": "header", "name": "X-Admin-Token"}
    },
    "schemas": {
      "Error": {"type": "object", "required": ["status", "reason_code", "message"],
        "properties": {"status": {"type": "string", "enum": ["error"]}, "reason_code": {"type": "string", "example": "PLAN_LIMIT"}, "message": {"type": "string"}}},
      "Limits": {"type": "object", "properties": {"max_checks": {"type": "integer"}, "webhooks": {"type": "boolean"}, "email": {"type": "boolean"}, "price_usd_month": {"type": "number"}, "emails_per_day": {"type": "integer"}}},
      "Check": {"type": "object",
        "properties": {
          "id": {"type": "string", "format": "uuid"}, "name": {"type": "string"},
          "period_s": {"type": "integer"}, "grace_s": {"type": "integer"},
          "status": {"type": "string", "enum": ["new", "up", "late", "failed", "deleted"], "description": "new = armed, never pinged; up = pinged in time; late = no ping within period + grace; failed = the job called /fail"},
          "ping_url": {"type": "string", "format": "uri"}, "fail_url": {"type": "string", "format": "uri"},
          "created_at": {"type": "number"}, "last_ping_at": {"type": "number", "nullable": true},
          "due_at": {"type": "number", "nullable": true}, "miss_count": {"type": "integer"}}},
      "Target": {"type": "object",
        "properties": {"id": {"type": "string"}, "kind": {"type": "string", "enum": ["webhook", "email"]}, "url": {"type": "string"}, "has_secret": {"type": "boolean"}, "created_at": {"type": "number"}}},
      "SignedEnvelope": {"type": "object", "required": ["event", "sig", "kid", "ts"],
        "description": "Signed bytes = canonical JSON of {event, kid, ts} (sorted keys, no whitespace), Ed25519. Verify with the key from GET /v1/keys.",
        "properties": {
          "event": {"type": "object", "properties": {"type": {"type": "string", "enum": ["check.late", "check.recovered"]}, "id": {"type": "string"}, "at": {"type": "integer"}, "service": {"type": "string", "enum": ["job-heartbeat"]}, "data": {"type": "object"}}},
          "sig": {"type": "string"}, "kid": {"type": "string"}, "ts": {"type": "integer"}}},
      "PingResult": {"type": "object",
        "properties": {"ok": {"type": "boolean"}, "dropped": {"type": "boolean", "description": "true when the per-check limit (10/min) was hit; the ping was ignored but still answered 200"}, "status": {"type": "string"}, "recovered": {"type": "boolean"}}}
    },
    "responses": {
      "Unauthorized": {"description": "Missing or unknown API key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "NotFound": {"description": "No such object for this account", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "BadRequest": {"description": "Validation failed (reason_code BAD_REQUEST or MALFORMED_JSON)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "PlanLimit": {"description": "The plan does not allow this (reason_code PLAN_LIMIT); upgrade via POST /v1/billing/checkout", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "RateLimited": {"description": "Too many requests; see Retry-After", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
    }
  },
  "paths": {
    "/v1/signup": {"post": {"tags": ["account"], "summary": "Create a free account; the API key is returned once", "operationId": "signup",
      "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["email"], "properties": {"email": {"type": "string", "format": "email"}}}}}},
      "responses": {
        "201": {"description": "Account created", "content": {"application/json": {"schema": {"type": "object", "properties": {"account_id": {"type": "string"}, "plan": {"type": "string"}, "api_key": {"type": "string", "description": "shown exactly once"}, "note": {"type": "string"}, "limits": {"$ref": "#/components/schemas/Limits"}}}}}},
        "400": {"$ref": "#/components/responses/BadRequest"}, "409": {"description": "An account already exists for this email"}, "429": {"$ref": "#/components/responses/RateLimited"}}}},
    "/v1/account": {"get": {"tags": ["account"], "summary": "Plan, limits, usage, billing mode", "operationId": "account", "security": [{"apiKey": []}],
      "responses": {"200": {"description": "OK"}, "401": {"$ref": "#/components/responses/Unauthorized"}}}},
    "/v1/billing/checkout": {"post": {"tags": ["account"], "summary": "Start the $7/month upgrade: returns a hosted Stripe Checkout URL", "operationId": "checkout", "security": [{"apiKey": []}],
      "requestBody": {"required": false, "content": {"application/json": {"schema": {"type": "object", "properties": {"success_url": {"type": "string", "format": "uri"}, "cancel_url": {"type": "string", "format": "uri"}}}}}},
      "responses": {"200": {"description": "Checkout session", "content": {"application/json": {"schema": {"type": "object", "properties": {"checkout_url": {"type": "string", "format": "uri"}, "session_id": {"type": "string"}, "price_usd_month": {"type": "number", "example": 7}}}}}},
        "401": {"$ref": "#/components/responses/Unauthorized"}, "502": {"description": "Stripe refused the request"}, "503": {"description": "Billing is not configured on this deployment"}}}},
    "/v1/billing/webhook": {"post": {"tags": ["account"], "summary": "Stripe webhook endpoint (called by Stripe, not by you)", "operationId": "stripeWebhook",
      "description": "Verifies Stripe-Signature and flips the plan on checkout.session.completed / customer.subscription.deleted. Idempotent.",
      "responses": {"200": {"description": "Received"}, "400": {"description": "Bad signature or malformed event"}, "503": {"description": "Billing is not configured"}}}},
    "/v1/checks": {
      "post": {"tags": ["checks"], "summary": "Create a check", "operationId": "createCheck", "security": [{"apiKey": []}],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["name", "period_s"],
          "properties": {"name": {"type": "string", "maxLength": 120}, "period_s": {"type": "integer", "minimum": 60, "maximum": 2592000, "description": "how often the job runs, in seconds (60 s to 30 days)"}, "grace_s": {"type": "integer", "minimum": 0, "maximum": 2592000, "default": 0, "description": "how late is still fine, in seconds"}}}}}},
        "responses": {"201": {"description": "Created; armed at once, so the first ping must arrive within period + grace", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Check"}}}},
          "400": {"$ref": "#/components/responses/BadRequest"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "402": {"$ref": "#/components/responses/PlanLimit"}}},
      "get": {"tags": ["checks"], "summary": "List checks", "operationId": "listChecks", "security": [{"apiKey": []}],
        "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"checks": {"type": "array", "items": {"$ref": "#/components/schemas/Check"}}}}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}}}},
    "/v1/checks/{id}": {
      "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}],
      "get": {"tags": ["checks"], "summary": "One check", "operationId": "getCheck", "security": [{"apiKey": []}],
        "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Check"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"$ref": "#/components/responses/NotFound"}}},
      "delete": {"tags": ["checks"], "summary": "Cancel a check (its 30-day event log stays readable)", "operationId": "deleteCheck", "security": [{"apiKey": []}],
        "responses": {"204": {"description": "Deleted"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"$ref": "#/components/responses/NotFound"}}}},
    "/v1/checks/{id}/events": {"get": {"tags": ["checks"], "summary": "Last 100 signed events with delivery results", "operationId": "checkEvents", "security": [{"apiKey": []}],
      "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}],
      "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"events": {"type": "array", "items": {"type": "object", "properties": {"seq": {"type": "integer"}, "type": {"type": "string"}, "at": {"type": "number"}, "envelope": {"$ref": "#/components/schemas/SignedEnvelope"}, "delivery": {"type": "array", "items": {"type": "object"}}}}}}}}}},
        "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"$ref": "#/components/responses/NotFound"}}}},
    "/v1/targets": {
      "post": {"tags": ["targets"], "summary": "Add an alert target", "operationId": "addTarget", "security": [{"apiKey": []}],
        "description": "kind=webhook needs the paid plan; url must be a public http(s) address. kind=email must be this account's own address (it is alerted anyway; the target only makes it explicit). Max 10 targets.",
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["kind", "url"],
          "properties": {"kind": {"type": "string", "enum": ["webhook", "email"]}, "url": {"type": "string", "description": "webhook URL, or the account's own email address"}, "secret": {"type": "string", "maxLength": 256, "description": "optional; enables X-AOKit-Signature HMAC on deliveries"}}}}}},
        "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Target"}}}},
          "400": {"$ref": "#/components/responses/BadRequest"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "402": {"$ref": "#/components/responses/PlanLimit"}}},
      "get": {"tags": ["targets"], "summary": "List targets", "operationId": "listTargets", "security": [{"apiKey": []}],
        "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"targets": {"type": "array", "items": {"$ref": "#/components/schemas/Target"}}}}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}}}},
    "/v1/targets/{id}": {"delete": {"tags": ["targets"], "summary": "Remove a target", "operationId": "deleteTarget", "security": [{"apiKey": []}],
      "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}],
      "responses": {"204": {"description": "Removed"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"$ref": "#/components/responses/NotFound"}}}},
    "/ping/{id}": {
      "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}, "description": "the check id from ping_url"}],
      "get": {"tags": ["ping"], "summary": "Sign of life (no auth; body ignored)", "operationId": "pingGet",
        "responses": {"200": {"description": "Recorded (or dropped by the 10/min per-check limit)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PingResult"}}}}, "404": {"description": "No such check"}}},
      "post": {"tags": ["ping"], "summary": "Sign of life (no auth; body ignored)", "operationId": "pingPost",
        "responses": {"200": {"description": "Recorded (or dropped by the 10/min per-check limit)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PingResult"}}}}, "404": {"description": "No such check"}}}},
    "/ping/{id}/fail": {
      "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}],
      "get": {"tags": ["ping"], "summary": "The job knows it broke: mark it failed now and send check.late", "operationId": "failGet",
        "responses": {"200": {"description": "Marked failed"}, "404": {"description": "No such check"}}},
      "post": {"tags": ["ping"], "summary": "The job knows it broke: mark it failed now and send check.late", "operationId": "failPost",
        "responses": {"200": {"description": "Marked failed"}, "404": {"description": "No such check"}}}},
    "/v1/keys": {"get": {"tags": ["verify"], "summary": "Public Ed25519 key for verifying signed events", "operationId": "keys",
      "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"keys": {"type": "array", "items": {"type": "object", "properties": {"kid": {"type": "string"}, "alg": {"type": "string"}, "public_key_hex": {"type": "string"}}}}, "service": {"type": "string"}}}}}}}}},
    "/v1/admin/metrics": {"get": {"tags": ["ops"], "summary": "Operator counts for this service only", "operationId": "adminMetrics", "security": [{"adminToken": []}],
      "responses": {"200": {"description": "Counts"}, "401": {"description": "Admin token required"}, "404": {"description": "Endpoint disabled (no admin token configured)"}}}},
    "/health": {"get": {"tags": ["ops"], "summary": "Health: ticker, billing mode, deadline stats, signing key id", "operationId": "health",
      "responses": {"200": {"description": "ok"}, "503": {"description": "degraded"}}}}
  }
}
