{
  "openapi": "3.0.3",
  "info": {
    "title": "Know What’s Due API",
    "version": "1.0.0",
    "description": "Read and write one person's tracked dates in Know What’s Due, on their behalf and with their consent.\n\nKnow What’s Due answers one question: what important things in somebody's life need attention again, and when. An obligation — the term this API uses throughout — is a passport, a registration, a filter: **a name, a date, optionally how often it comes round, and how much warning the person wants.** There is nothing to classify and no category to choose.\n\n**Every request acts as exactly one person.** The account is taken from the access token and cannot be named in a path, a query or a body — there is no parameter for it. Row level security in Postgres enforces this independently of this API, so a token only ever reaches the data of the account that approved it.\n\n**Dates are plain `YYYY-MM-DD` in the account's own time zone**, never the caller's and never UTC. Do not compute `overdue` or days-until yourself: every list response carries the `today` and `timezone` it was judged against, and every obligation carries `due_in_days`, `overdue` and `needs_attention` already resolved.\n\n**Completion is the core loop.** Completing something that repeats rolls its `next_due_date` forward from the day it was actually done and leaves it active; completing something that does not repeat finishes it. Which of the two happens is decided by one thing: whether `recurrence` is present.\n\nSo \"remember that my passport expires October 1, 2031\" is `{ name, next_due_date }` and nothing else, and \"my furnace filter needs changing December 20 and then every 3 months\" is the same plus `recurrence_value: 3, recurrence_unit: \"months\"`.\n\n**Nothing here is ever permanently deleted.** `DELETE` archives.\n\n**Know What’s Due is not a document vault.** It stores names, dates and intervals — never document scans, passport or licence numbers, account numbers, or other sensitive document contents. Do not write such values into `name` or `notes`.",
    "contact": {
      "name": "Know What’s Due",
      "url": "https://www.knowwhatsdue.com"
    }
  },
  "servers": [
    {
      "url": "https://www.knowwhatsdue.com/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "oauth2": []
    }
  ],
  "tags": [
    {
      "name": "Obligations",
      "description": "Things that need attention again, and when."
    }
  ],
  "paths": {
    "/obligations": {
      "get": {
        "tags": [
          "Obligations"
        ],
        "operationId": "listObligations",
        "summary": "List obligations",
        "description": "The account's obligations, soonest first.\n\nOne endpoint serves both \"what is coming up\" and \"what is overdue\" — they differ by a query parameter, and two endpoints would eventually disagree about the boundary. For upcoming work, pass `due_before`; for overdue work, pass `overdue=true`.\n\n**Requires the `read_obligations` capability** — \"See what you're tracking, when each thing is next due, and what's overdue\". Capabilities are granted to a connection when the person approves it; they are not OAuth scopes and cannot be requested in an authorization request. A connection without this capability receives `403 insufficient_capability`.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Which statuses to include. Defaults to `active`. `all` returns every status.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "completed",
                "archived",
                "all"
              ],
              "default": "active"
            }
          },
          {
            "name": "due_before",
            "in": "query",
            "required": false,
            "description": "Only obligations due on or before this date. Inclusive. `YYYY-MM-DD`.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "overdue",
            "in": "query",
            "required": false,
            "description": "When `true`, only obligations whose `next_due_date` is strictly before the account's today. Something due today is not overdue.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many to return at most. Defaults to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The matching obligations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ObligationList"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No credential, or a bearer token that did not verify. `unauthorized` when nothing was presented; `invalid_token` when a token was presented and failed verification — wrong issuer, expired, not a user access token, or not issued to a connected app.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "`insufficient_capability` when the connection lacks the capability this operation needs. `forbidden` when a browser sent a mutating request from another origin.",
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "description": "`invalid_request`. An unrecognised `status`, a `due_before` that is not a real calendar date, an `overdue` that is not `true` or `false`, or a `limit` out of range.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "`internal_error`.",
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "tags": [
          "Obligations"
        ],
        "operationId": "createObligation",
        "summary": "Add an obligation",
        "description": "Creates one obligation for the account the token acts as.\n\nOnly `name` and `next_due_date` are required. Send `recurrence_value` and `recurrence_unit` together when it repeats, and omit both when it does not — that single choice is what decides what completing it will later do.\n\nDo not attempt to classify the thing. There is no category field and nothing infers one.\n\n**Requires the `write_obligations` capability** — \"Add obligations, change them, mark them complete, and archive them\". Capabilities are granted to a connection when the person approves it; they are not OAuth scopes and cannot be requested in an authorization request. A connection without this capability receives `403 insufficient_capability`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ObligationCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ObligationEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "`malformed_body`. The body was not a JSON object.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "No credential, or a bearer token that did not verify. `unauthorized` when nothing was presented; `invalid_token` when a token was presented and failed verification — wrong issuer, expired, not a user access token, or not issued to a connected app.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "`insufficient_capability` when the connection lacks the capability this operation needs. `forbidden` when a browser sent a mutating request from another origin.",
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "description": "`invalid_request`. A missing or empty `name`, a `next_due_date` that is not a real calendar date, half an interval, or an out-of-range value. The message names the field.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "`internal_error`.",
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/obligations/{id}": {
      "patch": {
        "tags": [
          "Obligations"
        ],
        "operationId": "updateObligation",
        "summary": "Change an obligation",
        "description": "Updates the fields named in the body and leaves the rest alone.\n\nThe result is validated as a whole obligation, not as the fields that changed, so a patch cannot compose a row a create would have refused.\n\nTo stop something repeating, send **both** `recurrence_value` and `recurrence_unit` as `null`. Sending one without the other is a contradiction and is refused. Omitting them leaves the interval untouched.\n\n**Requires the `write_obligations` capability** — \"Add obligations, change them, mark them complete, and archive them\". Capabilities are granted to a connection when the person approves it; they are not OAuth scopes and cannot be requested in an authorization request. A connection without this capability receives `403 insufficient_capability`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The obligation's id, as returned by a list or a create.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ObligationPatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated obligation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ObligationEnvelope"
                }
              }
            }
          },
          "400": {
            "description": "`malformed_body`.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "No credential, or a bearer token that did not verify. `unauthorized` when nothing was presented; `invalid_token` when a token was presented and failed verification — wrong issuer, expired, not a user access token, or not issued to a connected app.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "`insufficient_capability` when the connection lacks the capability this operation needs. `forbidden` when a browser sent a mutating request from another origin.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "`not_found`. No such obligation, or it belongs to another account — deliberately the same answer, so an id cannot be tested for existence.",
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "description": "`invalid_request`. As for create.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "`internal_error`.",
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": [
          "Obligations"
        ],
        "operationId": "archiveObligation",
        "summary": "Archive an obligation",
        "description": "**This archives rather than permanently deleting.** The obligation's `status` becomes `archived`; the row and its history remain, and it can be found again with `status=archived` or `status=all`.\n\nThere is no operation in this API that permanently removes anything. A connection cannot destroy the record of what somebody was tracking; only the person can, from Know What’s Due itself.\n\nArchiving something already archived is not an error.\n\n**Requires the `write_obligations` capability** — \"Add obligations, change them, mark them complete, and archive them\". Capabilities are granted to a connection when the person approves it; they are not OAuth scopes and cannot be requested in an authorization request. A connection without this capability receives `403 insufficient_capability`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The obligation's id, as returned by a list or a create.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The archived obligation, with `status` now `archived`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ObligationEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or a bearer token that did not verify. `unauthorized` when nothing was presented; `invalid_token` when a token was presented and failed verification — wrong issuer, expired, not a user access token, or not issued to a connected app.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "`insufficient_capability` when the connection lacks the capability this operation needs. `forbidden` when a browser sent a mutating request from another origin.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "`not_found`. No such obligation, or it belongs to another account — deliberately the same answer, so an id cannot be tested for existence.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "`internal_error`.",
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/obligations/{id}/complete": {
      "post": {
        "tags": [
          "Obligations"
        ],
        "operationId": "completeObligation",
        "summary": "Mark an obligation complete",
        "description": "Records that this was done, and decides what happens next by whether the obligation repeats.\n\n**If it repeats** — it has a `recurrence` — `next_due_date` moves forward one interval **from the day it was completed, not from the day it was due**, `last_completed_on` is set, and the status stays `active`. A filter due September 1 and changed September 19, on a three month cycle, is next due December 19.\n\n**If it does not repeat**, `last_completed_on` is set, `next_due_date` is left where it was, and the status becomes `completed`.\n\nThe response's `repeated` says which happened, so there is no need to infer it by comparing dates.\n\nThe body is optional. `completed_on` may be backdated to record something done earlier; it may not be in the future.\n\n**Requires the `write_obligations` capability** — \"Add obligations, change them, mark them complete, and archive them\". Capabilities are granted to a connection when the person approves it; they are not OAuth scopes and cannot be requested in an authorization request. A connection without this capability receives `403 insufficient_capability`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The obligation's id, as returned by a list or a create.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "completed_on": {
                    "type": "string",
                    "format": "date",
                    "description": "The day it was done, in the account's time zone. Defaults to the account's today. Cannot be in the future."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Completed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "obligation",
                        "repeated",
                        "next_due_date"
                      ],
                      "properties": {
                        "obligation": {
                          "$ref": "#/components/schemas/Obligation"
                        },
                        "repeated": {
                          "type": "boolean",
                          "description": "True when this rolled forward and stayed active; false when it was finished."
                        },
                        "next_due_date": {
                          "type": "string",
                          "format": "date",
                          "nullable": true,
                          "description": "The date it moved to, when it repeated. Null when it did not."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`malformed_body`.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "No credential, or a bearer token that did not verify. `unauthorized` when nothing was presented; `invalid_token` when a token was presented and failed verification — wrong issuer, expired, not a user access token, or not issued to a connected app.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "`insufficient_capability` when the connection lacks the capability this operation needs. `forbidden` when a browser sent a mutating request from another origin.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "`not_found`. No such obligation, or it belongs to another account — deliberately the same answer, so an id cannot be tested for existence.",
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "description": "`conflict`. The obligation is archived, or is already completed. Restore it in Know What’s Due first.",
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "description": "`invalid_request`. `completed_on` is not a real calendar date, or is in the future.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "`internal_error`.",
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.1 authorization code flow with PKCE, required. The authorization server is Supabase Auth; Know What’s Due hosts the consent screen the person approves on.\n\nClients must be registered in advance — dynamic client registration is not enabled. Redirect URIs must match exactly.\n\nPresent the access token as `Authorization: Bearer <token>`. The scopes below are the only ones that exist and govern the ID token; what a connection may do in this API is set per connection when it is approved.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://qbvdhpnftrsyqxfpjlnj.supabase.co/auth/v1/oauth/authorize",
            "tokenUrl": "https://qbvdhpnftrsyqxfpjlnj.supabase.co/auth/v1/oauth/token",
            "refreshUrl": "https://qbvdhpnftrsyqxfpjlnj.supabase.co/auth/v1/oauth/token",
            "scopes": {
              "openid": "Issue an OpenID Connect ID token",
              "email": "Include the email address in the ID token",
              "profile": "Include profile claims in the ID token",
              "offline_access": "Issue a refresh token"
            }
          }
        }
      }
    },
    "responses": {
      "Error": {
        "description": "An error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "Every failure has this shape. `code` is part of the contract and is what a caller should branch on; `message` is written for a person and may be reworded.",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "forbidden",
                  "insufficient_capability",
                  "invalid_token",
                  "not_found",
                  "malformed_body",
                  "invalid_request",
                  "conflict",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "Recurrence": {
        "type": "object",
        "nullable": true,
        "required": [
          "value",
          "unit",
          "label"
        ],
        "description": "How often this comes round. Null when it does not repeat. The two halves are always present together.",
        "properties": {
          "value": {
            "type": "integer",
            "minimum": 1,
            "maximum": 999
          },
          "unit": {
            "type": "string",
            "enum": [
              "days",
              "weeks",
              "months",
              "years"
            ]
          },
          "label": {
            "type": "string",
            "description": "Ready to say out loud — \"Every 3 months\", \"Every year\".",
            "example": "Every 3 months"
          }
        }
      },
      "Obligation": {
        "type": "object",
        "required": [
          "id",
          "name",
          "status",
          "next_due_date",
          "due_in_days",
          "overdue",
          "needs_attention",
          "recurrence",
          "reminder_days_before",
          "last_completed_on",
          "notes",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "maxLength": 120,
            "example": "Passport"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "completed",
              "archived"
            ]
          },
          "next_due_date": {
            "type": "string",
            "format": "date"
          },
          "due_in_days": {
            "type": "integer",
            "description": "Days until due, in the account's time zone. Negative when overdue. Computed server-side — do not recalculate it."
          },
          "overdue": {
            "type": "boolean",
            "description": "Active and past its date. Something due today is not overdue."
          },
          "needs_attention": {
            "type": "boolean",
            "description": "Active and within `reminder_days_before` of its date, or already past it. This is what Know What’s Due shows first."
          },
          "recurrence": {
            "$ref": "#/components/schemas/Recurrence"
          },
          "reminder_days_before": {
            "type": "integer",
            "minimum": 0,
            "maximum": 365,
            "description": "How far ahead this starts counting as needing attention. It sorts and labels; it sends nothing. Know What’s Due has no notifications."
          },
          "last_completed_on": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "maxLength": 500
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ObligationList": {
        "type": "object",
        "required": [
          "today",
          "timezone",
          "count",
          "obligations"
        ],
        "properties": {
          "today": {
            "type": "string",
            "format": "date",
            "description": "The account's current date, in its own time zone. Every `due_in_days` and `overdue` in this response was judged against it."
          },
          "timezone": {
            "type": "string",
            "description": "The account's IANA time zone.",
            "example": "America/Chicago"
          },
          "count": {
            "type": "integer",
            "description": "How many obligations are in this response."
          },
          "obligations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Obligation"
            }
          }
        }
      },
      "ObligationEnvelope": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "obligation"
            ],
            "properties": {
              "obligation": {
                "$ref": "#/components/schemas/Obligation"
              }
            }
          }
        }
      },
      "ObligationCreate": {
        "type": "object",
        "required": [
          "name",
          "next_due_date"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120,
            "example": "Passport"
          },
          "next_due_date": {
            "type": "string",
            "format": "date"
          },
          "recurrence_value": {
            "type": "integer",
            "minimum": 1,
            "maximum": 999,
            "description": "Send together with `recurrence_unit`, or omit both. Present means it repeats."
          },
          "recurrence_unit": {
            "type": "string",
            "enum": [
              "days",
              "weeks",
              "months",
              "years"
            ],
            "description": "Send together with `recurrence_value`."
          },
          "reminder_days_before": {
            "type": "integer",
            "minimum": 0,
            "maximum": 365,
            "default": 30
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "maxLength": 500,
            "description": "A short note. Never document numbers, account numbers or other sensitive document contents."
          }
        }
      },
      "ObligationPatch": {
        "type": "object",
        "description": "Every field is optional. Omitted fields are left unchanged.",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "next_due_date": {
            "type": "string",
            "format": "date"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "completed",
              "archived"
            ]
          },
          "recurrence_value": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "maximum": 999,
            "description": "Send with `recurrence_unit` as null to stop repeating."
          },
          "recurrence_unit": {
            "type": "string",
            "nullable": true,
            "enum": [
              "days",
              "weeks",
              "months",
              "years"
            ]
          },
          "reminder_days_before": {
            "type": "integer",
            "minimum": 0,
            "maximum": 365
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "maxLength": 500
          }
        }
      }
    }
  },
  "x-product-capabilities": {
    "description": "This product's own vocabulary for what a connection may do. Not OAuth scopes: no client can request one, and nothing in a token carries one. They are attached to a connection when the person approves it, and each operation above says which one it needs.",
    "capabilities": {
      "read_obligations": "See what you're tracking, when each thing is next due, and what's overdue",
      "write_obligations": "Add obligations, change them, mark them complete, and archive them"
    }
  }
}