openapi: 3.1.0 info: title: Facet Creator API version: "1.0.0" summary: Read, write, and subscribe to your Facet site's content and events. description: | The Facet Creator API lets you program against your own Facet site: read your portfolio, posts, projects, courses and talks; create and edit all content types (posts, projects, courses, talks, experience, education, skills, and certifications); pull member and analytics data; and receive (or poll for) events such as new purchases and course completions. ## Authentication Every request authenticates with an API key sent as a Bearer token: ``` Authorization: Bearer fct_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Keys are created in your site's admin panel under **Tools -> API**. A key carries one or more **scopes** that limit what it can do, and may optionally restrict which web origins can use it (`allowed_origins`). The full key value is shown only once at creation time -- store it somewhere safe. There is no `X-API-Key` header: the only accepted credential is the `Authorization: Bearer` header above. ## Scopes Scopes are stamped on the key and checked per endpoint. The admin "create key" form accepts three scope values: `read`, `write`, and `webhooks`. - `read` -- read your published content (v1 read endpoints, webhook polling). - `write` -- create, update, and delete posts, projects, courses, talks, experience, education, skills, and certifications (v1 write endpoints). - `webhooks` -- register outbound webhooks. The v2 endpoints check namespaced scope strings (`read:content`, `read:members`, `read:analytics`, `write:webhooks`). Each operation below documents the exact scope string its handler requires. ## Base URL Replace `your-site` with your Facet subdomain (or use your custom domain): ``` https://your-site.facetcloud.io ``` All paths are relative to that origin. ## Versions - **v1** (`/api/v1/*`) -- the stable surface that also renders the public site. Read endpoints plus create/update/delete for posts and projects. List endpoints use opaque **keyset (cursor) pagination**. - **v2** (`/api/v2/*`) -- the "program against Facet" surface intended for SDKs and automations. Uses simple **page / per_page** pagination. ## Errors Every error response is a JSON object with a single `error` string: ```json { "error": "insufficient scope" } ``` See the `Error` schema and the per-status descriptions on each operation. ## Rate limits Public endpoints are limited per client IP to roughly **60 requests per minute** (burst 10). Admin key-management and webhook-management endpoints use a tighter **10 requests per minute** (burst 5) tier. Checkout-style and password endpoints (not part of this API) use a **5 per minute** tier. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (a Unix timestamp). A `429` response also carries a `Retry-After` header (seconds). The webhook polling endpoint additionally enforces a per-key cap of **20 polls per minute**. contact: name: Facet Cloud url: https://get-facet.com/developers license: name: Proprietary url: https://get-facet.com/terms servers: - url: https://{site}.facetcloud.io description: A creator's Facet site (replace {site} with your subdomain). variables: site: default: your-site description: Your Facet subdomain. tags: - name: Profile description: The site owner's public profile. - name: Posts description: Blog / newsletter posts. Readable on v1 and v2; writable on v1. - name: Projects description: Portfolio projects. Readable on v1 and v2; writable on v1. - name: Courses description: Courses (metadata only over the API). Writable on v1. - name: Talks description: Talks and speaking engagements. Writable on v1. - name: Portfolio description: Experience, education, skills, and certifications. All writable on v1. - name: Members description: Member accounts (v2). - name: Analytics description: Site analytics (v2). - name: Webhooks description: >- Outbound event delivery: register subscriptions, poll for events, and manage delivery. Events are signed with HMAC-SHA256. - name: API keys description: Admin endpoints for creating and managing API keys. # --------------------------------------------------------------------------- # Security # --------------------------------------------------------------------------- security: - BearerApiKey: [] paths: # ========================================================================= # v1 READ # ========================================================================= /api/v1/profile: get: tags: [Profile] operationId: getProfile summary: Get the owner's public profile description: | Returns the site owner's public profile fields. Requires the `read` scope. security: - BearerApiKey: [read] responses: "200": description: The profile. content: application/json: schema: $ref: "#/components/schemas/Profile" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/posts: get: tags: [Posts] operationId: listPostsV1 summary: List published posts (keyset pagination) description: | Returns public, non-draft posts newest-first by default. Use `next_cursor` from the response as the `cursor` query parameter to fetch the next page. Requires the `read` scope. Paid posts omit the `content` field unless the reader has purchased them; over the API, paid-post `content` is always omitted. security: - BearerApiKey: [read] parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Sort" responses: "200": description: A page of posts. content: application/json: schema: $ref: "#/components/schemas/PostListV1" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" post: tags: [Posts] operationId: createPostV1 summary: Create a post description: | Creates a post. Only the allow-listed fields are accepted (`title`, `slug`, `content`, `excerpt`, `visibility`, `is_draft`, `published_at`, `featured`, `tags`, `access_tier`). Requires the `write` scope. security: - BearerApiKey: [write] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PostWrite" responses: "201": description: The post was created. content: application/json: schema: $ref: "#/components/schemas/CreatedRecord" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/posts/{idOrSlug}: # NOTE: the live backend exposes GET /api/v1/posts/{slug} and # PATCH|DELETE /api/v1/posts/{id} -- the same URL template with a different # path-parameter name. OpenAPI forbids two path items that differ only by # parameter name, so they are merged here into one path. The path segment is # resolved as a SLUG by GET and as a record ID by PATCH/DELETE (see each # operation's parameter description). get: tags: [Posts] operationId: getPostBySlugV1 summary: Get a published post by slug description: Returns one public, non-draft post by its slug. Requires `read`. security: - BearerApiKey: [read] parameters: - name: idOrSlug in: path required: true description: The post **slug** (GET resolves this segment as a slug). schema: type: string responses: "200": description: The post. content: application/json: schema: $ref: "#/components/schemas/Post" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" patch: tags: [Posts] operationId: updatePostV1 summary: Update a post description: | Updates the allow-listed fields of a post by its record id. Requires the `write` scope. security: - BearerApiKey: [write] parameters: - name: idOrSlug in: path required: true description: The post **record id** (PATCH resolves this segment as an id). schema: type: string requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PostWrite" responses: "200": $ref: "#/components/responses/StatusUpdated" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" delete: tags: [Posts] operationId: deletePostV1 summary: Delete a post description: Deletes a post by its record id. Requires the `write` scope. security: - BearerApiKey: [write] parameters: - name: idOrSlug in: path required: true description: The post **record id** (DELETE resolves this segment as an id). schema: type: string responses: "200": $ref: "#/components/responses/StatusDeleted" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/projects: get: tags: [Projects] operationId: listProjectsV1 summary: List published projects (keyset pagination) description: | Returns public, non-draft projects newest-first by default. Use `next_cursor` for the next page. Requires the `read` scope. security: - BearerApiKey: [read] parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Sort" responses: "200": description: A page of projects. content: application/json: schema: $ref: "#/components/schemas/ProjectListV1" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" post: tags: [Projects] operationId: createProjectV1 summary: Create a project description: | Creates a project. Only allow-listed fields are accepted (`title`, `slug`, `description`, `url`, `repo_url`, `status`, `visibility`, `is_draft`, `tech_stack`, `tags`). Requires the `write` scope. security: - BearerApiKey: [write] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ProjectWrite" responses: "201": description: The project was created. content: application/json: schema: $ref: "#/components/schemas/CreatedRecord" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/projects/{idOrSlug}: # Merged for the same reason as /api/v1/posts/{idOrSlug}: the backend serves # GET .../{slug} and PATCH|DELETE .../{id} on one URL template. GET resolves # the segment as a slug; PATCH/DELETE resolve it as a record id. get: tags: [Projects] operationId: getProjectBySlugV1 summary: Get a published project by slug description: Returns one public, non-draft project by its slug. Requires `read`. security: - BearerApiKey: [read] parameters: - name: idOrSlug in: path required: true description: The project **slug** (GET resolves this segment as a slug). schema: type: string responses: "200": description: The project. content: application/json: schema: $ref: "#/components/schemas/Project" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" patch: tags: [Projects] operationId: updateProjectV1 summary: Update a project description: Updates allow-listed fields of a project by id. Requires `write`. security: - BearerApiKey: [write] parameters: - name: idOrSlug in: path required: true description: The project **record id** (PATCH resolves this segment as an id). schema: type: string requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ProjectWrite" responses: "200": $ref: "#/components/responses/StatusUpdated" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" delete: tags: [Projects] operationId: deleteProjectV1 summary: Delete a project description: Deletes a project by id. Requires the `write` scope. security: - BearerApiKey: [write] parameters: - name: idOrSlug in: path required: true description: The project **record id** (DELETE resolves this segment as an id). schema: type: string responses: "200": $ref: "#/components/responses/StatusDeleted" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/experience: get: tags: [Portfolio] operationId: listExperience summary: List work experience description: | Returns all public experience entries (no pagination). Requires `read`. security: - BearerApiKey: [read] responses: "200": description: Experience entries. content: application/json: schema: $ref: "#/components/schemas/SimpleListExperience" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" post: tags: [Portfolio] operationId: createExperienceV1 summary: Create a work experience entry description: | Creates a work experience entry. Only allow-listed fields are accepted (`company`, `title`, `location`, `start_date`, `end_date`, `description`, `bullets`, `skills`, `visibility`, `is_draft`, `sort_order`). Requires the `write` scope. security: - BearerApiKey: [write] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExperienceWrite" responses: "201": description: The experience entry was created. content: application/json: schema: $ref: "#/components/schemas/CreatedRecord" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/education: get: tags: [Portfolio] operationId: listEducation summary: List education description: Returns all education entries (no pagination). Requires `read`. security: - BearerApiKey: [read] responses: "200": description: Education entries. content: application/json: schema: $ref: "#/components/schemas/SimpleListEducation" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" post: tags: [Portfolio] operationId: createEducationV1 summary: Create an education entry description: | Creates an education entry. Only allow-listed fields are accepted (`institution`, `degree`, `field`, `start_date`, `end_date`, `description`, `visibility`, `is_draft`, `sort_order`). Requires the `write` scope. security: - BearerApiKey: [write] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EducationWrite" responses: "201": description: The education entry was created. content: application/json: schema: $ref: "#/components/schemas/CreatedRecord" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/skills: get: tags: [Portfolio] operationId: listSkills summary: List skills description: Returns all skills (no pagination). Requires `read`. security: - BearerApiKey: [read] responses: "200": description: Skills. content: application/json: schema: $ref: "#/components/schemas/SimpleListSkill" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" post: tags: [Portfolio] operationId: createSkillV1 summary: Create a skill description: | Creates a skill. Only allow-listed fields are accepted (`name`, `category`, `proficiency`, `visibility`, `sort_order`). Requires the `write` scope. security: - BearerApiKey: [write] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SkillWrite" responses: "201": description: The skill was created. content: application/json: schema: $ref: "#/components/schemas/CreatedRecord" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/talks: get: tags: [Talks] operationId: listTalksV1 summary: List talks description: Returns all talks newest-first (no pagination). Requires `read`. security: - BearerApiKey: [read] responses: "200": description: Talks. content: application/json: schema: $ref: "#/components/schemas/SimpleListTalk" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" post: tags: [Talks] operationId: createTalkV1 summary: Create a talk description: | Creates a talk. Only allow-listed fields are accepted (`title`, `event`, `event_url`, `date`, `location`, `description`, `slides_url`, `video_url`, `visibility`, `is_draft`, `sort_order`). Requires the `write` scope. security: - BearerApiKey: [write] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TalkWrite" responses: "201": description: The talk was created. content: application/json: schema: $ref: "#/components/schemas/CreatedRecord" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/talks/{id}: patch: tags: [Talks] operationId: updateTalkV1 summary: Update a talk description: Updates a talk by its record id. Requires the `write` scope. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TalkWrite" responses: "200": $ref: "#/components/responses/StatusUpdated" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" delete: tags: [Talks] operationId: deleteTalkV1 summary: Delete a talk description: Deletes a talk by its record id. Requires the `write` scope. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": $ref: "#/components/responses/StatusDeleted" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/courses: get: tags: [Courses] operationId: listCoursesV1 summary: List courses (keyset pagination) description: | Returns public, non-draft courses (metadata only) newest-first. Use `next_cursor` for the next page. Requires the `read` scope. security: - BearerApiKey: [read] parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Sort" responses: "200": description: A page of courses. content: application/json: schema: $ref: "#/components/schemas/CourseListV1" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" post: tags: [Courses] operationId: createCourseV1 summary: Create a course description: | Creates a course. Only allow-listed fields are accepted (`title`, `slug`, `description`, `instructor_name`, `instructor_bio`, `access_tier`, `price`, `sequential_access`, `difficulty`, `estimated_hours`, `tags`, `learning_outcomes`, `visibility`, `is_draft`, `sort_order`). Requires the `write` scope. security: - BearerApiKey: [write] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CourseWrite" responses: "201": description: The course was created. content: application/json: schema: $ref: "#/components/schemas/CreatedRecord" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/courses/{id}: patch: tags: [Courses] operationId: updateCourseV1 summary: Update a course description: | Updates the allow-listed fields of a course by its record id. Requires the `write` scope. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CourseWrite" responses: "200": $ref: "#/components/responses/StatusUpdated" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" delete: tags: [Courses] operationId: deleteCourseV1 summary: Delete a course description: Deletes a course by its record id. Requires the `write` scope. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": $ref: "#/components/responses/StatusDeleted" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/certifications: get: tags: [Portfolio] operationId: listCertifications summary: List certifications description: Returns all certifications newest-first. Requires `read`. security: - BearerApiKey: [read] responses: "200": description: Certifications. content: application/json: schema: $ref: "#/components/schemas/SimpleListCertification" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" post: tags: [Portfolio] operationId: createCertificationV1 summary: Create a certification description: | Creates a certification. Only allow-listed fields are accepted (`name`, `issuer`, `issue_date`, `expiry_date`, `credential_id`, `credential_url`, `visibility`, `is_draft`, `sort_order`). Requires the `write` scope. security: - BearerApiKey: [write] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CertificationWrite" responses: "201": description: The certification was created. content: application/json: schema: $ref: "#/components/schemas/CreatedRecord" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/certifications/{id}: patch: tags: [Portfolio] operationId: updateCertificationV1 summary: Update a certification description: Updates a certification by its record id. Requires `write`. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CertificationWrite" responses: "200": $ref: "#/components/responses/StatusUpdated" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" delete: tags: [Portfolio] operationId: deleteCertificationV1 summary: Delete a certification description: Deletes a certification by its record id. Requires `write`. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": $ref: "#/components/responses/StatusDeleted" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/experience/{id}: patch: tags: [Portfolio] operationId: updateExperienceV1 summary: Update a work experience entry description: | Updates a work experience entry by its record id. Only allow-listed fields are accepted (`company`, `title`, `location`, `start_date`, `end_date`, `description`, `bullets`, `skills`, `visibility`, `is_draft`, `sort_order`). Requires the `write` scope. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExperienceWrite" responses: "200": $ref: "#/components/responses/StatusUpdated" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" delete: tags: [Portfolio] operationId: deleteExperienceV1 summary: Delete a work experience entry description: Deletes a work experience entry by its record id. Requires `write`. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": $ref: "#/components/responses/StatusDeleted" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/education/{id}: patch: tags: [Portfolio] operationId: updateEducationV1 summary: Update an education entry description: Updates an education entry by its record id. Requires `write`. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EducationWrite" responses: "200": $ref: "#/components/responses/StatusUpdated" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" delete: tags: [Portfolio] operationId: deleteEducationV1 summary: Delete an education entry description: Deletes an education entry by its record id. Requires `write`. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": $ref: "#/components/responses/StatusDeleted" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/skills/{id}: patch: tags: [Portfolio] operationId: updateSkillV1 summary: Update a skill description: Updates a skill by its record id. Requires `write`. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SkillWrite" responses: "200": $ref: "#/components/responses/StatusUpdated" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" delete: tags: [Portfolio] operationId: deleteSkillV1 summary: Delete a skill description: Deletes a skill by its record id. Requires `write`. security: - BearerApiKey: [write] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": $ref: "#/components/responses/StatusDeleted" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" # ========================================================================= # v1 WEBHOOK POLLING (Zapier / n8n / Make / Pipedream) # ========================================================================= /api/v1/webhooks/events: get: tags: [Webhooks] operationId: listWebhookEvents summary: List pollable event types description: | Lists the event keys the polling endpoint understands. Used to populate a trigger dropdown in tools like Zapier. Requires the `read` scope. security: - BearerApiKey: [read] responses: "200": description: The event catalog. content: application/json: schema: $ref: "#/components/schemas/WebhookEventCatalog" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /api/v1/webhooks/poll/{event}: get: tags: [Webhooks] operationId: pollWebhookEvents summary: Poll for events of a given type description: | Returns logged events for `{event}`. By default events are newest-first; pass `cursor` (from a previous `next_cursor`) to page through them, or pass `since` (an RFC 3339 timestamp) to replay oldest-first. Each event carries a stable `id` for de-duplication. Requires the `read` scope. In addition to the standard rate limit, this endpoint enforces a per-key cap of 20 polls per minute. Events are retained for 7 days. security: - BearerApiKey: [read] parameters: - name: event in: path required: true description: The event key to poll (e.g. `purchase.completed`). schema: $ref: "#/components/schemas/WebhookEventName" - name: cursor in: query required: false description: An opaque cursor from a previous response's `next_cursor`. schema: type: string - name: since in: query required: false description: >- An RFC 3339 timestamp. Returns events at or after this time, oldest-first. Ignored when `cursor` is also supplied. schema: type: string format: date-time - name: limit in: query required: false description: Maximum events to return (default 50, max 200). schema: type: integer minimum: 1 maximum: 200 default: 50 responses: "200": description: A batch of events. content: application/json: schema: $ref: "#/components/schemas/WebhookPollResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" # ========================================================================= # v2 # ========================================================================= /api/v2/posts: get: tags: [Posts] operationId: listPostsV2 summary: List posts (page pagination) description: | Returns public, non-draft posts newest-first using page / per_page pagination. Requires the `read:content` scope. security: - BearerApiKey: [read:content] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PerPage" responses: "200": description: A page of posts. content: application/json: schema: $ref: "#/components/schemas/PostListV2" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/ServerError" /api/v2/posts/{id}: get: tags: [Posts] operationId: getPostV2 summary: Get a post by id description: | Returns one public, non-draft post by record id. Requires `read:content`. security: - BearerApiKey: [read:content] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": description: The post. content: application/json: schema: $ref: "#/components/schemas/PostV2" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /api/v2/projects: get: tags: [Projects] operationId: listProjectsV2 summary: List projects (page pagination) description: | Returns public, non-draft projects newest-first. Requires `read:content`. security: - BearerApiKey: [read:content] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PerPage" responses: "200": description: A page of projects. content: application/json: schema: $ref: "#/components/schemas/ProjectListV2" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/ServerError" /api/v2/projects/{id}: get: tags: [Projects] operationId: getProjectV2 summary: Get a project by id description: Returns one public, non-draft project by id. Requires `read:content`. security: - BearerApiKey: [read:content] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": description: The project. content: application/json: schema: $ref: "#/components/schemas/ProjectV2" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /api/v2/courses: get: tags: [Courses] operationId: listCoursesV2 summary: List courses (page pagination) description: | Returns public, non-draft courses (metadata only) newest-first. Requires the `read:content` scope. security: - BearerApiKey: [read:content] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PerPage" responses: "200": description: A page of courses. content: application/json: schema: $ref: "#/components/schemas/CourseListV2" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/ServerError" /api/v2/talks: get: tags: [Talks] operationId: listTalksV2 summary: List talks (page pagination) description: Returns public, non-draft talks newest-first. Requires `read:content`. security: - BearerApiKey: [read:content] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PerPage" responses: "200": description: A page of talks. content: application/json: schema: $ref: "#/components/schemas/TalkListV2" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/ServerError" /api/v2/members: get: tags: [Members] operationId: listMembersV2 summary: List member accounts (page pagination) description: | Returns member accounts newest-first. Requires the `read:members` scope. security: - BearerApiKey: [read:members] parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PerPage" responses: "200": description: A page of members. content: application/json: schema: $ref: "#/components/schemas/MemberListV2" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/ServerError" /api/v2/analytics: get: tags: [Analytics] operationId: getAnalyticsV2 summary: Get site analytics description: | Returns the same analytics payload as the admin analytics screen, but authorized by the API key's `read:analytics` scope. The exact fields depend on your plan: advanced analytics are included only when your plan allows them. Pass an optional `period` to choose the window. security: - BearerApiKey: [read:analytics] parameters: - name: period in: query required: false description: >- Reporting window. Accepted values are normalized server-side (for example `7d`, `30d`, `90d`); unknown values fall back to the default window. schema: type: string responses: "200": description: The analytics payload (shape varies by plan). content: application/json: schema: $ref: "#/components/schemas/AnalyticsResponse" "401": $ref: "#/components/responses/Unauthorized" "403": description: >- The key lacks `read:analytics`, or analytics is not available on the current plan. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/ServerError" /api/v2/webhooks: post: tags: [Webhooks] operationId: registerWebhookV2 summary: Register an outbound webhook description: | Registers a webhook subscription that will receive event deliveries. Provide an `https://` URL and a list of `events` (event names or the namespace wildcards `analytics.*`, `member.*`, `payment.*`, `content.*`, `course.*`, or `*` for everything). Supply your own `secret` for HMAC signing, or omit it to have one generated and returned. Requires the `write:webhooks` scope. security: - BearerApiKey: [write:webhooks] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebhookRegisterRequest" responses: "201": description: The webhook was registered. content: application/json: schema: $ref: "#/components/schemas/WebhookRegisterResponse" "400": description: >- Missing/invalid URL (must be `https://`), empty `events`, or an event name outside the supported set. content: application/json: schema: $ref: "#/components/schemas/Error" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "503": description: >- The webhook storage collection has not been provisioned on this tenant yet. content: application/json: schema: $ref: "#/components/schemas/Error" callbacks: eventDelivery: "{$request.body#/url}": post: summary: Event delivery to your registered URL description: | When a subscribed event fires, Facet POSTs this body to the URL you registered. The request also carries these headers: - `X-Facet-Event` -- the event name. - `X-Facet-Delivery` -- a unique delivery id. - `X-Facet-Signature` -- `sha256=`, the HMAC-SHA256 of the raw request body keyed with your subscription secret. - `User-Agent: Facet-Webhook/1.0`. Respond with any 2xx status to acknowledge. Non-2xx responses are retried on a back-off schedule (1m, 5m, 30m, 2h, 12h); a subscription is auto-disabled after 10 consecutive failures. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebhookPayload" responses: "2XX": description: Your endpoint acknowledged receipt. # ========================================================================= # ADMIN: API KEYS # ========================================================================= /api/admin/api-keys: post: tags: [API keys] operationId: createApiKey summary: Create an API key description: | Creates a new API key and returns the full key value **once** -- it cannot be retrieved again. This endpoint is part of the admin surface and requires a logged-in site owner session (not an API key). security: - AdminSession: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ApiKeyCreateRequest" responses: "201": description: The created key (full value shown once). content: application/json: schema: $ref: "#/components/schemas/ApiKeyCreateResponse" "400": description: Missing name or an invalid scope. content: application/json: schema: $ref: "#/components/schemas/Error" "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/TooManyRequests" get: tags: [API keys] operationId: listApiKeys summary: List API keys description: | Lists all API keys for the site. Only the key prefix is returned -- never the full key. Requires a site owner session. security: - AdminSession: [] responses: "200": description: The keys. content: application/json: schema: $ref: "#/components/schemas/ApiKeyListResponse" "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/TooManyRequests" /api/admin/api-keys/{id}: patch: tags: [API keys] operationId: updateApiKey summary: Update an API key description: | Updates a key's name, scopes, allowed origins, active flag, or expiry. Requires a site owner session. security: - AdminSession: [] parameters: - $ref: "#/components/parameters/RecordId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ApiKeyUpdateRequest" responses: "200": $ref: "#/components/responses/StatusUpdated" "400": description: An invalid scope was supplied. content: application/json: schema: $ref: "#/components/schemas/Error" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" delete: tags: [API keys] operationId: revokeApiKey summary: Revoke (delete) an API key description: Permanently deletes a key. Requires a site owner session. security: - AdminSession: [] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": $ref: "#/components/responses/StatusDeleted" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /api/admin/api-keys/{id}/rotate: post: tags: [API keys] operationId: rotateApiKey summary: Rotate an API key's secret description: | Issues a new `fct_live_*` secret for an existing key and returns it **once** -- it cannot be retrieved again. The previous secret is NOT immediately revoked: it is kept valid for a **24-hour grace window** so that in-flight integrations continue working while the caller redeploys the new secret. After 24 hours the old secret is refused. This endpoint authenticates the same way as the other key-management routes: it requires a logged-in site owner session, not an API key. The `key_prefix` in the list endpoint is rewritten to the new secret's prefix immediately; both old and new secrets share the row during the grace window. security: - AdminSession: [] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": description: The new key (full value shown once) and rotation metadata. content: application/json: schema: $ref: "#/components/schemas/ApiKeyRotateResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" "500": $ref: "#/components/responses/ServerError" # ========================================================================= # ADMIN: WEBHOOK SUBSCRIPTIONS # ========================================================================= /api/admin/webhooks: post: tags: [Webhooks] operationId: createWebhookSubscription summary: Create a webhook subscription description: | Creates a webhook subscription and returns a generated HMAC signing `secret`. The `events` list must contain only the supported concrete event names. Requires a site owner session. security: - AdminSession: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebhookSubscriptionCreateRequest" responses: "201": description: The subscription was created. content: application/json: schema: $ref: "#/components/schemas/WebhookSubscriptionCreateResponse" "400": description: Missing/invalid URL, empty events, or an invalid event type. content: application/json: schema: $ref: "#/components/schemas/Error" "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/TooManyRequests" get: tags: [Webhooks] operationId: listWebhookSubscriptions summary: List webhook subscriptions description: Lists all webhook subscriptions. Requires a site owner session. security: - AdminSession: [] responses: "200": description: The subscriptions. content: application/json: schema: $ref: "#/components/schemas/WebhookSubscriptionListResponse" "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/TooManyRequests" /api/admin/webhooks/{id}: patch: tags: [Webhooks] operationId: updateWebhookSubscription summary: Update a webhook subscription description: | Updates a subscription's URL, events, or active flag. Re-enabling resets the failure counter. Requires a site owner session. security: - AdminSession: [] parameters: - $ref: "#/components/parameters/RecordId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/WebhookSubscriptionUpdateRequest" responses: "200": $ref: "#/components/responses/StatusUpdated" "400": description: An invalid URL was supplied. content: application/json: schema: $ref: "#/components/schemas/Error" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" delete: tags: [Webhooks] operationId: deleteWebhookSubscription summary: Delete a webhook subscription description: Permanently deletes a subscription. Requires a site owner session. security: - AdminSession: [] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": $ref: "#/components/responses/StatusDeleted" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /api/admin/webhooks/{id}/test: post: tags: [Webhooks] operationId: testWebhookSubscription summary: Send a test event description: | Delivers a synthetic `test` event to the subscription's URL and returns the HTTP status code it responded with. Requires a site owner session. security: - AdminSession: [] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": description: The delivery result. content: application/json: schema: $ref: "#/components/schemas/WebhookTestResult" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /api/admin/webhooks/{id}/deliveries: get: tags: [Webhooks] operationId: listWebhookDeliveries summary: List delivery history description: | Returns the most recent delivery attempts for a subscription (newest first), including status, attempt count, last status code, and any error. Requires a site owner session. security: - AdminSession: [] parameters: - $ref: "#/components/parameters/RecordId" responses: "200": description: Delivery records. content: application/json: schema: $ref: "#/components/schemas/WebhookDeliveryListResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Components # --------------------------------------------------------------------------- components: securitySchemes: BearerApiKey: type: http scheme: bearer bearerFormat: fct_live_* description: | A Facet API key sent as `Authorization: Bearer fct_live_...`. Create and manage keys in your site's admin panel. The scopes required by each operation are listed in its `security` entry. AdminSession: type: apiKey in: header name: Authorization description: | Admin endpoints (`/api/admin/*`) require a logged-in **site owner** session token, not an API key. These are the endpoints your own admin panel calls; they are documented here for completeness. parameters: RecordId: name: id in: path required: true description: The PocketBase record id (a 15-character alphanumeric string). schema: type: string Cursor: name: cursor in: query required: false description: >- An opaque keyset cursor taken from a previous response's `next_cursor`. Omit it to start from the first page. A malformed cursor is ignored and the response restarts from page one. schema: type: string Limit: name: limit in: query required: false description: Items per page (1-100). schema: type: integer minimum: 1 maximum: 100 default: 20 Sort: name: sort in: query required: false description: >- Sort order. One of `created`, `-created`, `updated`, `-updated`, `title`, `-title`. A leading `-` means descending. Defaults to `-created`. schema: type: string enum: [created, -created, updated, -updated, title, -title] default: -created Page: name: page in: query required: false description: 1-based page number. schema: type: integer minimum: 1 default: 1 PerPage: name: per_page in: query required: false description: Items per page (max 200). schema: type: integer minimum: 1 maximum: 200 default: 50 responses: Unauthorized: description: >- Missing or malformed `Authorization` header, or an invalid / unknown API key. content: application/json: schema: $ref: "#/components/schemas/Error" examples: missingHeader: value: { "error": "missing or invalid Authorization header" } invalidKey: value: { "error": "invalid API key" } Forbidden: description: >- The key is valid but disabled, expired, lacks the required scope, or the request origin is not allowed. content: application/json: schema: $ref: "#/components/schemas/Error" examples: scope: value: { "error": "insufficient scope" } disabled: value: { "error": "API key is disabled" } expired: value: { "error": "API key has expired" } origin: value: { "error": "origin not allowed" } NotFound: description: The requested resource does not exist (or is not public). content: application/json: schema: $ref: "#/components/schemas/Error" example: { "error": "not found" } BadRequest: description: The request was malformed (e.g. invalid body or missing parameter). content: application/json: schema: $ref: "#/components/schemas/Error" example: { "error": "invalid request body" } TooManyRequests: description: >- Rate limit exceeded. Inspect `Retry-After` (seconds) and the `X-RateLimit-*` headers. headers: Retry-After: description: Seconds to wait before retrying. schema: type: integer X-RateLimit-Limit: description: Maximum requests allowed in the window (the burst size). schema: type: integer X-RateLimit-Remaining: description: Requests remaining in the current window. schema: type: integer X-RateLimit-Reset: description: Unix timestamp (seconds) when the window resets. schema: type: integer content: application/json: schema: $ref: "#/components/schemas/Error" example: { "error": "too many requests" } ServerError: description: An unexpected server error (e.g. the underlying query failed). content: application/json: schema: $ref: "#/components/schemas/Error" example: { "error": "query failed" } StatusUpdated: description: The record was updated. content: application/json: schema: $ref: "#/components/schemas/StatusResponse" example: { "status": "updated" } StatusDeleted: description: The record was deleted. content: application/json: schema: $ref: "#/components/schemas/StatusResponse" example: { "status": "deleted" } schemas: Error: type: object description: The standard error envelope for every error response. properties: error: type: string description: A human-readable error message. required: [error] additionalProperties: false StatusResponse: type: object properties: status: type: string examples: ["updated", "deleted"] required: [status] CreatedRecord: type: object description: Returned after creating a post or project via v1 write endpoints. properties: id: type: string description: The new record's id. created: type: string format: date-time description: Creation timestamp. required: [id] Profile: type: object properties: name: { type: string } headline: { type: string } summary: { type: string } location: { type: string } email: { type: string, format: email } avatar_url: { type: string } website: { type: string } github: { type: string } linkedin: { type: string } twitter: { type: string } cta_text: { type: string } cta_url: { type: string } cta_enabled: { type: boolean } updated: { type: string, format: date-time } Post: type: object description: A post as returned by the v1 read endpoints. properties: id: { type: string } title: { type: string } slug: { type: string } excerpt: { type: string } visibility: $ref: "#/components/schemas/Visibility" published_at: { type: string } featured: { type: boolean } created: { type: string, format: date-time } updated: { type: string, format: date-time } content: type: string description: >- Full body. Omitted for paid posts (`access_tier: paid`) over the API. tags: $ref: "#/components/schemas/Tags" PostV2: type: object description: A post as returned by the v2 endpoints (always includes content). properties: id: { type: string } title: { type: string } slug: { type: string } excerpt: { type: string } content: { type: string } tags: $ref: "#/components/schemas/Tags" published_at: { type: string } created: { type: string, format: date-time } updated: { type: string, format: date-time } access_tier: $ref: "#/components/schemas/AccessTier" price: type: integer description: Price in the smallest currency unit (e.g. cents). PostWrite: type: object description: >- Writable post fields. Unknown fields are ignored; only the listed fields are persisted. properties: title: { type: string } slug: { type: string } content: { type: string } excerpt: { type: string } visibility: $ref: "#/components/schemas/Visibility" is_draft: { type: boolean } published_at: { type: string } featured: { type: boolean } tags: $ref: "#/components/schemas/Tags" access_tier: $ref: "#/components/schemas/AccessTier" additionalProperties: false Project: type: object description: A project as returned by the v1 read endpoints. properties: id: { type: string } title: { type: string } slug: { type: string } description: { type: string } url: { type: string } repo_url: { type: string } status: { type: string } visibility: $ref: "#/components/schemas/Visibility" created: { type: string, format: date-time } updated: { type: string, format: date-time } tech_stack: type: array items: { type: string } tags: $ref: "#/components/schemas/Tags" ProjectV2: type: object description: A project as returned by the v2 endpoints. properties: id: { type: string } title: { type: string } slug: { type: string } description: { type: string } tags: $ref: "#/components/schemas/Tags" created: { type: string, format: date-time } updated: { type: string, format: date-time } access_tier: $ref: "#/components/schemas/AccessTier" price: { type: integer } ProjectWrite: type: object description: Writable project fields. Unknown fields are ignored. properties: title: { type: string } slug: { type: string } description: { type: string } url: { type: string } repo_url: { type: string } status: { type: string } visibility: $ref: "#/components/schemas/Visibility" is_draft: { type: boolean } tech_stack: type: array items: { type: string } tags: $ref: "#/components/schemas/Tags" additionalProperties: false Course: type: object description: Course metadata (v1 read). properties: id: { type: string } title: { type: string } slug: { type: string } description: { type: string } instructor_name: { type: string } access_tier: $ref: "#/components/schemas/AccessTier" price: { type: integer } difficulty: { type: string } estimated_hours: { type: number } visibility: $ref: "#/components/schemas/Visibility" created: { type: string, format: date-time } updated: { type: string, format: date-time } tags: $ref: "#/components/schemas/Tags" learning_outcomes: type: array items: { type: string } CourseV2: type: object description: Course metadata (v2). properties: id: { type: string } title: { type: string } slug: { type: string } description: { type: string } price: { type: integer } trial_days: { type: integer } access_tier: $ref: "#/components/schemas/AccessTier" created: { type: string, format: date-time } Talk: type: object description: A talk (v1 read). properties: id: { type: string } title: { type: string } slug: { type: string } event: { type: string } date: { type: string } url: { type: string } video_url: { type: string } description: { type: string } TalkV2: type: object description: A talk (v2). properties: id: { type: string } title: { type: string } slug: { type: string } event: { type: string } date: { type: string } location: { type: string } Experience: type: object properties: id: { type: string } company: { type: string } title: { type: string } location: { type: string } start_date: { type: string } end_date: { type: string } is_current: { type: boolean } description: { type: string } sort_order: { type: integer } Education: type: object properties: id: { type: string } institution: { type: string } degree: { type: string } field: { type: string } start_date: { type: string } end_date: { type: string } description: { type: string } Skill: type: object properties: id: { type: string } name: { type: string } category: { type: string } level: { type: string } sort_order: { type: integer } Certification: type: object properties: id: { type: string } name: { type: string } issuer: { type: string } date: { type: string } expiry_date: { type: string } credential_id: { type: string } credential_url: { type: string } Member: type: object description: A member account (v2). properties: id: { type: string } email: { type: string, format: email } name: { type: string } plan: { type: string } status: { type: string } created: { type: string, format: date-time } AnalyticsResponse: type: object description: >- The analytics payload. The exact set of fields depends on your plan, so this is documented as a free-form object. additionalProperties: true Visibility: type: string description: Content visibility. Only `public` content is returned by the API. enum: [public, private, unlisted] AccessTier: type: string description: Whether the content is free or requires purchase. enum: [free, paid] Tags: type: array description: Free-form tags. May be absent when a record has no tags. items: type: string # --- v1 list envelopes (keyset pagination) --- KeysetListMeta: type: object properties: total: type: integer description: Number of items in THIS page (not the grand total). page: type: integer description: Always 1 for keyset lists; pages advance via `next_cursor`. per_page: type: integer description: The effective page size (the `limit`). next_cursor: type: string description: >- Cursor for the next page. Absent on the final page. Pass it back as the `cursor` query parameter. PostListV1: allOf: - $ref: "#/components/schemas/KeysetListMeta" - type: object properties: data: type: array items: $ref: "#/components/schemas/Post" required: [data] ProjectListV1: allOf: - $ref: "#/components/schemas/KeysetListMeta" - type: object properties: data: type: array items: $ref: "#/components/schemas/Project" required: [data] CourseListV1: allOf: - $ref: "#/components/schemas/KeysetListMeta" - type: object properties: data: type: array items: $ref: "#/components/schemas/Course" required: [data] # --- v1 simple lists (no pagination) --- SimpleListExperience: type: object properties: data: type: array items: $ref: "#/components/schemas/Experience" required: [data] SimpleListEducation: type: object properties: data: type: array items: $ref: "#/components/schemas/Education" required: [data] SimpleListSkill: type: object properties: data: type: array items: $ref: "#/components/schemas/Skill" required: [data] SimpleListTalk: type: object properties: data: type: array items: $ref: "#/components/schemas/Talk" required: [data] SimpleListCertification: type: object properties: data: type: array items: $ref: "#/components/schemas/Certification" required: [data] # --- v2 list envelopes (page pagination) --- PageListMeta: type: object properties: page: type: integer description: The page number returned. per_page: type: integer description: The effective page size. PostListV2: allOf: - $ref: "#/components/schemas/PageListMeta" - type: object properties: items: type: array items: $ref: "#/components/schemas/PostV2" required: [items] ProjectListV2: allOf: - $ref: "#/components/schemas/PageListMeta" - type: object properties: items: type: array items: $ref: "#/components/schemas/ProjectV2" required: [items] CourseListV2: allOf: - $ref: "#/components/schemas/PageListMeta" - type: object properties: items: type: array items: $ref: "#/components/schemas/CourseV2" required: [items] TalkListV2: allOf: - $ref: "#/components/schemas/PageListMeta" - type: object properties: items: type: array items: $ref: "#/components/schemas/TalkV2" required: [items] MemberListV2: allOf: - $ref: "#/components/schemas/PageListMeta" - type: object properties: items: type: array items: $ref: "#/components/schemas/Member" required: [items] # --- Webhook event names --- WebhookEventName: type: string description: One of the nine concrete event types Facet emits. enum: - content.created - content.updated - content.deleted - content.published - purchase.completed - course.enrolled - course.completed - testimonial.submitted - newsletter.subscribe WebhookRegisterEvent: type: string description: >- An event name accepted by `POST /api/v2/webhooks`. Concrete event names, namespace wildcards, or `*` for everything. enum: - "*" - "analytics.*" - "member.*" - "payment.*" - "content.*" - "course.*" - content.created - content.updated - content.deleted - content.published - purchase.completed - course.enrolled - course.completed - testimonial.submitted - newsletter.subscribe WebhookEventCatalog: type: object properties: data: type: array items: type: object properties: key: $ref: "#/components/schemas/WebhookEventName" label: type: string required: [data] WebhookPayload: type: object description: >- The body POSTed to subscribed URLs and returned by the poll endpoint. The HTTP delivery also carries `X-Facet-Event`, `X-Facet-Delivery`, and an `X-Facet-Signature: sha256=` HMAC of the raw JSON body. properties: event: $ref: "#/components/schemas/WebhookEventName" timestamp: type: string format: date-time description: RFC 3339 time the event fired. data: type: object description: Event-specific fields (see the event catalog). additionalProperties: true required: [event, timestamp, data] WebhookPollEvent: type: object properties: id: type: string description: Stable event id for de-duplication. event: $ref: "#/components/schemas/WebhookEventName" timestamp: type: string format: date-time data: type: object additionalProperties: true required: [id, event, timestamp, data] WebhookPollResponse: type: object properties: data: type: array items: $ref: "#/components/schemas/WebhookPollEvent" count: type: integer next_cursor: type: string description: Cursor for the next poll. Empty string when there are no more. required: [data, count, next_cursor] WebhookRegisterRequest: type: object properties: url: type: string format: uri description: An `https://` URL to receive deliveries. events: type: array minItems: 1 items: $ref: "#/components/schemas/WebhookRegisterEvent" secret: type: string description: >- Optional HMAC signing secret. If omitted, one is generated and returned. required: [url, events] WebhookRegisterResponse: type: object properties: id: { type: string } url: { type: string, format: uri } events: type: array items: $ref: "#/components/schemas/WebhookRegisterEvent" secret: type: string description: The signing secret (generated if you did not supply one). required: [id, url, events, secret] CourseWrite: type: object description: >- Writable course fields. Unknown fields are ignored; only the listed fields are persisted. properties: title: { type: string } slug: { type: string } description: { type: string } instructor_name: { type: string } instructor_bio: { type: string } access_tier: $ref: "#/components/schemas/AccessTier" price: { type: integer, description: "Price in the smallest currency unit (e.g. cents)." } sequential_access: { type: boolean } difficulty: { type: string } estimated_hours: { type: number } tags: $ref: "#/components/schemas/Tags" learning_outcomes: type: array items: { type: string } visibility: $ref: "#/components/schemas/Visibility" is_draft: { type: boolean } sort_order: { type: integer } additionalProperties: false TalkWrite: type: object description: Writable talk fields. Unknown fields are ignored. properties: title: { type: string } event: { type: string } event_url: { type: string } date: { type: string } location: { type: string } description: { type: string } slides_url: { type: string } video_url: { type: string } visibility: $ref: "#/components/schemas/Visibility" is_draft: { type: boolean } sort_order: { type: integer } additionalProperties: false ExperienceWrite: type: object description: Writable work experience fields. Unknown fields are ignored. properties: company: { type: string } title: { type: string } location: { type: string } start_date: { type: string } end_date: { type: string } description: { type: string } bullets: type: array items: { type: string } skills: type: array items: { type: string } visibility: $ref: "#/components/schemas/Visibility" is_draft: { type: boolean } sort_order: { type: integer } additionalProperties: false EducationWrite: type: object description: Writable education fields. Unknown fields are ignored. properties: institution: { type: string } degree: { type: string } field: { type: string } start_date: { type: string } end_date: { type: string } description: { type: string } visibility: $ref: "#/components/schemas/Visibility" is_draft: { type: boolean } sort_order: { type: integer } additionalProperties: false SkillWrite: type: object description: Writable skill fields. Unknown fields are ignored. properties: name: { type: string } category: { type: string } proficiency: { type: string } visibility: $ref: "#/components/schemas/Visibility" sort_order: { type: integer } additionalProperties: false CertificationWrite: type: object description: Writable certification fields. Unknown fields are ignored. properties: name: { type: string } issuer: { type: string } issue_date: { type: string } expiry_date: { type: string } credential_id: { type: string } credential_url: { type: string } visibility: $ref: "#/components/schemas/Visibility" is_draft: { type: boolean } sort_order: { type: integer } additionalProperties: false ApiKeyRotateResponse: type: object description: >- The response to a key rotation. The new full key is shown once and cannot be retrieved again. The old secret remains valid until `previous_key_expires_at` (24 hours after rotation). properties: id: { type: string } name: { type: string } key: type: string description: >- The new full key value. Shown ONCE; cannot be retrieved again. Always starts with `fct_live_`. key_prefix: { type: string } previous_key_expires_at: type: string format: date-time description: >- The RFC 3339 timestamp when the old secret's 24-hour grace window closes. After this instant only the new key authenticates. message: { type: string } required: [id, key, previous_key_expires_at] # --- Admin: API keys --- ApiKeyScope: type: string description: A scope accepted by the admin "create key" form. enum: [read, write, webhooks] ApiKeyCreateRequest: type: object properties: name: type: string description: A human label for the key. scopes: type: array description: Defaults to `["read"]` if omitted. items: $ref: "#/components/schemas/ApiKeyScope" allowed_origins: type: array description: >- Optional list of web origins allowed to use the key. When set, the request's `Origin` (or `Referer`) must match one of these. items: type: string expires_at: type: string format: date-time description: Optional expiry timestamp. required: [name] ApiKeyCreateResponse: type: object properties: id: { type: string } name: { type: string } key: type: string description: The full key value. Shown ONCE; cannot be retrieved again. key_prefix: { type: string } scopes: type: array items: $ref: "#/components/schemas/ApiKeyScope" is_active: { type: boolean } created: { type: string, format: date-time } message: { type: string } required: [id, key] ApiKeySummary: type: object properties: id: { type: string } name: { type: string } key_prefix: { type: string } scopes: type: array items: $ref: "#/components/schemas/ApiKeyScope" allowed_origins: type: array items: { type: string } is_active: { type: boolean } last_used_at: { type: string } request_count: { type: integer } expires_at: { type: string } created: { type: string, format: date-time } ApiKeyListResponse: type: object properties: data: type: array items: $ref: "#/components/schemas/ApiKeySummary" required: [data] ApiKeyUpdateRequest: type: object properties: name: { type: string } scopes: type: array items: $ref: "#/components/schemas/ApiKeyScope" allowed_origins: type: array items: { type: string } is_active: { type: boolean } expires_at: { type: string, format: date-time } # --- Admin: webhook subscriptions --- WebhookSubscriptionCreateRequest: type: object properties: url: type: string format: uri events: type: array minItems: 1 items: $ref: "#/components/schemas/WebhookEventName" required: [url, events] WebhookSubscriptionCreateResponse: type: object properties: id: { type: string } url: { type: string, format: uri } events: type: array items: $ref: "#/components/schemas/WebhookEventName" secret: type: string description: The generated HMAC signing secret. required: [id, url, events, secret] WebhookSubscription: type: object properties: id: { type: string } url: { type: string, format: uri } events: type: array items: $ref: "#/components/schemas/WebhookEventName" is_active: { type: boolean } failure_count: { type: integer } last_success_at: { type: string } last_failure_at: { type: string } last_status_code: { type: integer } created: { type: string, format: date-time } WebhookSubscriptionListResponse: type: object properties: data: type: array items: $ref: "#/components/schemas/WebhookSubscription" required: [data] WebhookSubscriptionUpdateRequest: type: object properties: url: { type: string, format: uri } events: type: array items: $ref: "#/components/schemas/WebhookEventName" is_active: { type: boolean } WebhookTestResult: type: object properties: status_code: type: integer description: The HTTP status the receiving URL responded with. success: { type: boolean } error: type: string description: Present only when delivery failed. required: [status_code, success] WebhookDelivery: type: object properties: id: { type: string } event_type: $ref: "#/components/schemas/WebhookEventName" status: type: string description: One of `pending`, `delivered`, `failed`, `exhausted`. enum: [pending, delivered, failed, exhausted] attempts: { type: integer } last_status_code: { type: integer } last_error: { type: string } delivered_at: { type: string } next_retry_at: { type: string } created: { type: string, format: date-time } WebhookDeliveryListResponse: type: object properties: data: type: array items: $ref: "#/components/schemas/WebhookDelivery" required: [data]