{"openapi":"3.1.0","info":{"title":"pkgplane API","version":"v0.1.0"},"components":{"securitySchemes":{"ApiKey":{"type":"http","scheme":"bearer","description":"An API key (`pkp_…`) from the API keys page"}},"schemas":{"Health":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded"]},"version":{"type":"string","example":"v0.3.0+f16784d"},"builtAt":{"type":"string","example":"2026-10-31T14:23Z"},"db":{"type":"string","enum":["ok","unavailable"]}},"required":["status","version","builtAt","db"]},"User":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"createdAt":{"type":"string","format":"date-time"}},"required":["id","name","email","createdAt"]},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","example":"Not found"},"issues":{"type":"array","items":{"type":"object","properties":{"path":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"message":{"type":"string"}},"required":["path","message"]}}},"required":["message"]}},"required":["error"]},"ApiKey":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"start":{"type":"string","example":"pkp_9f86d081"},"status":{"type":"string","enum":["active","disabled","expired"]},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"},"disabledAt":{"type":["string","null"],"format":"date-time"}},"required":["id","name","description","start","status","createdAt","lastUsedAt","expiresAt","disabledAt"]},"CreatedApiKey":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"type":"object","properties":{"key":{"type":"string","description":"The full key. Only returned here."}},"required":["key"]}]},"CreateApiKey":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64,"example":"CI deploys"},"description":{"type":"string","maxLength":500,"default":""},"expiresInDays":{"type":["integer","null"],"minimum":1,"maximum":365,"description":"Leave out or null for a key that never expires"}},"required":["name"]},"UpdateApiKey":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"type":"string","maxLength":500},"enabled":{"type":"boolean"}}},"Invitation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"invitedBy":{"type":["string","null"],"description":"Inviter's email"},"status":{"type":"string","enum":["pending","expired","accepted"]},"createdAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"acceptedAt":{"type":["string","null"],"format":"date-time"},"acceptedBy":{"type":["string","null"],"description":"The account's current email (null once it's deleted)"}},"required":["id","email","invitedBy","status","createdAt","expiresAt","acceptedAt","acceptedBy"]},"CreateInvitation":{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email"}},"required":["email"]},"InvitationLookup":{"type":"object","properties":{"email":{"type":"string","format":"email"},"expiresAt":{"type":"string","format":"date-time"}},"required":["email","expiresAt"]},"LookupInvitation":{"type":"object","properties":{"token":{"type":"string","pattern":"^[0-9a-f]{64}$"}},"required":["token"]},"AcceptInvitation":{"type":"object","properties":{"token":{"type":"string","pattern":"^[0-9a-f]{64}$"},"name":{"type":"string","minLength":1,"maxLength":100},"password":{"type":"string","minLength":8,"maxLength":128}},"required":["token","name","password"]}},"parameters":{}},"paths":{"/api/health":{"get":{"tags":["System"],"summary":"Health check","description":"Reports the build, and whether the database answers a trivial query.","responses":{"200":{"description":"The API and its database are up","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}},"503":{"description":"The API is up, but the database is unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}}}}},"/api/me":{"get":{"tags":["Auth"],"summary":"The signed-in user","security":[{"ApiKey":[]}],"responses":{"200":{"description":"The current user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many requests with this API key","headers":{"Retry-After":{"description":"Seconds to wait","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/api-keys":{"get":{"tags":["API keys"],"summary":"List your API keys, newest first","responses":{"200":{"description":"Your API keys","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["API keys"],"summary":"Create an API key","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKey"}}}},"responses":{"201":{"description":"The new key; `key` is not shown again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedApiKey"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Signed in by an admin impersonating you","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already 25 keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/api-keys/{id}":{"patch":{"tags":["API keys"],"summary":"Rename, describe, disable or enable an API key","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateApiKey"}}}},"responses":{"200":{"description":"The updated key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKey"}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No key with that id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid id or body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["API keys"],"summary":"Delete an API key","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No key with that id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/invitations":{"get":{"tags":["Invitations"],"summary":"List invites: pending and expired, then accepted (history)","responses":{"200":{"description":"Invites","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Invitation"}}}}},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not an admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Invitations"],"summary":"Invite someone by email (replaces their pending invite)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvitation"}}}},"responses":{"201":{"description":"Invite sent"},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not an admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A user with this email already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/invitations/{id}/resend":{"post":{"tags":["Invitations"],"summary":"Send an invite again with a new link (the old one stops working)","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Invite sent"},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not an admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No pending invite with that id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/invitations/{id}":{"delete":{"tags":["Invitations"],"summary":"Revoke an invite","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Revoked"},"401":{"description":"Not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not an admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No pending invite with that id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/invitations/lookup":{"post":{"tags":["Invitations"],"summary":"An invite's email, from its token","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LookupInvitation"}}}},"responses":{"200":{"description":"The invite","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationLookup"}}}},"404":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Malformed token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many requests from this IP","headers":{"Retry-After":{"description":"Seconds to wait","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/invitations/accept":{"post":{"tags":["Invitations"],"summary":"Accept an invite: create a verified account and sign in","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInvitation"}}}},"responses":{"201":{"description":"Account created; the response sets the session cookie"},"404":{"description":"Invalid or expired token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A user with this email already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid token, name or password","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many requests from this IP","headers":{"Retry-After":{"description":"Seconds to wait","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{}}