{
  "openapi": "3.1.0",
  "info": {
    "title": "Simple Socials API",
    "version": "1.0.0",
    "description": "API-first publishing to Facebook Pages and Instagram. Simple Socials is operated by GAU Web Solutions. Limits are per Workspace and can be raised by the operator: requests per minute (default 60; over it you get `429 rate_limited` with `Retry-After`), connected Social Accounts (default 10; see Connect Links) and max file size (default 250 MB; `media_too_large`). The current limits are in `GET /v1/workspace`. A Workspace disabled by the operator gets `403 workspace_disabled` on every endpoint, its Connect Links stop working and its scheduled work (publishing, media copies, webhook deliveries) is paused until it is re-enabled."
  },
  "servers": [
    {
      "url": "https://api.simplesocials.dev"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/workspace": {
      "get": {
        "operationId": "getWorkspace",
        "summary": "Get the current Workspace",
        "description": "Returns the Workspace that owns the API Key used to authenticate the request.",
        "tags": [
          "Workspace"
        ],
        "responses": {
          "200": {
            "description": "The Workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workspace"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/connect-links": {
      "post": {
        "operationId": "createConnectLink",
        "summary": "Create a Connect Link",
        "description": "Creates a single-use link to a hosted page where the owner of a social account signs in with the platform and chooses which accounts to attach to your Workspace. Send the returned `url` to them. The link expires after one hour and can complete once. When `redirect_url` is given, the person is sent back to it afterwards with `status=connected`, `connect_link_id` and `social_account_ids` (comma-separated), or with `status=error`, `connect_link_id` and `error` (one of `access_denied`, `missing_permissions`, `no_accounts`, `personal_account`, `nothing_selected`, `account_mismatch`, `platform_error`, `link_expired`, `link_used`, `invalid_state`, `account_limit_reached`, `workspace_disabled`). Your Workspace's Social Account limit (`limits.max_social_accounts`) is enforced when the person confirms their selection: accounts that are `active` or `needs_reauth` count, `disconnected` and deleted ones don't. Reconnecting an account that already counts is always allowed; if the new accounts would go over the limit, none are attached, the page answers 422 and the redirect carries `error=account_limit_reached` (the link stays usable, so they can choose fewer).\n\n**Reconnecting:** pass `social_account_id` to repair an existing Social Account (for example one that is `needs_reauth` or `disconnected`) in place. `provider` may then be omitted; if given it must match the account's. The person must sign in as that same platform account: the account picker offers only it, and signing in as anyone else fails with `error=account_mismatch` without changing anything (the link stays usable). On success the account is `active` again with the same ID and a new token.",
        "tags": [
          "Connect Links"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "provider": {
                    "type": "string",
                    "enum": [
                      "facebook",
                      "instagram"
                    ],
                    "description": "Platform to connect. `facebook` attaches Facebook Pages the person chooses. `instagram` attaches one Instagram professional account (Business or Creator, no Facebook Page needed) through Instagram Login; personal accounts are rejected with `error=personal_account`."
                  },
                  "redirect_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Optional absolute http(s) URL to return the person to when they finish or fail."
                  },
                  "social_account_id": {
                    "type": "string",
                    "description": "Reconnect this existing Social Account instead of connecting new ones. Required unless `provider` is given; an unknown or deleted account fails with `validation_failed` (`details.field` = `social_account_id`).",
                    "example": "sa_8x2kq4m7d3fz9ab5c6hjtn1p"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The Connect Link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectLink"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/social-accounts": {
      "get": {
        "operationId": "listSocialAccounts",
        "summary": "List Social Accounts",
        "description": "Lists the Workspace's connected Social Accounts. Paginate with `limit` and the opaque `cursor` from the previous page's `next_cursor`; `next_cursor` is null on the last page.",
        "tags": [
          "Social Accounts"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "`next_cursor` from the previous page."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of Social Accounts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SocialAccount"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/social-accounts/{id}": {
      "get": {
        "operationId": "getSocialAccount",
        "summary": "Get a Social Account",
        "tags": [
          "Social Accounts"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "sa_3kq7d2m5x9fz4ab6c8hjtn2p"
          }
        ],
        "responses": {
          "200": {
            "description": "The Social Account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialAccount"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "deleteSocialAccount",
        "summary": "Disconnect a Social Account",
        "description": "Destroys the account's stored token and removes it from the Workspace. Connecting the same account again later restores it with the same ID.",
        "tags": [
          "Social Accounts"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "sa_3kq7d2m5x9fz4ab6c8hjtn2p"
          }
        ],
        "responses": {
          "204": {
            "description": "Disconnected."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/posts": {
      "get": {
        "operationId": "listPosts",
        "summary": "List Posts",
        "description": "Lists your Workspace's Posts, newest first (by `created_at`), with their targets. Filters combine with AND; date ranges include `*_after` and exclude `*_before`, and take RFC 3339 date-times with an offset. An invalid filter fails with `validation_failed`, `details.field` naming it.",
        "tags": [
          "Posts"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "`next_cursor` from the previous page."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "scheduled",
                "publishing",
                "published",
                "failed",
                "partially_failed",
                "cancelled"
              ]
            },
            "description": "Only Posts whose overall `status` is this."
          },
          {
            "name": "social_account_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only Posts with a target on this Social Account."
          },
          {
            "name": "created_after",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only Posts created at or after this time."
          },
          {
            "name": "created_before",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only Posts created before this time."
          },
          {
            "name": "publish_after",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only Posts whose `publish_at` is at or after this time."
          },
          {
            "name": "publish_before",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only Posts whose `publish_at` is before this time."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of Posts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Post"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createPost",
        "summary": "Create a Post",
        "description": "Creates a Post and fans it out into one Post Target per Social Account. The call returns at once; each target is published by a background job. Poll `GET /v1/posts/{id}` for the outcome. Set `publish_at` to schedule the Post: its targets stay `scheduled` until then; without it (or with a time in the past) they publish as soon as possible. Cancel unpublished targets with `POST /v1/posts/{id}/cancel`. Content is validated against every target's platform first, with that platform's `overrides` applied (Facebook: a caption, a link, or both; or one to 10 photos (JPEG, PNG or GIF, at most 10 MB each; several become one multi-photo post); or one video (MP4 or MOV, at most 1 GB and 20 minutes); captions up to 63,206 characters, optional with Media; photos and videos can't be mixed, and posts with Media can't have a `link` (put it in the caption). Instagram: one JPEG image, one MP4/MOV video published as a Reel, or a carousel of 2–20 images and videos; images JPEG, at most 8 MB, aspect ratio 4:5 to 1.91:1; videos 3 s to 15 min, at most 300 MB; captions up to 2,200 characters, 30 hashtags and 20 @mentions; text-only posts are rejected, and so are Posts with a `link` unless `overrides.instagram.link` removes it (`\"\"` or `null`), naming the target). Every Media in `media_ids` must exist in your Workspace and be `ready`; Media that cleanup has deleted fails with 422 `media_deleted` (`details.field` names it), so upload it again. A problem fails the whole request with `validation_failed`, where `details.field` names the field and `details.social_account_id` the offending target. Targeting a Social Account that is `needs_reauth` fails with 422 `account_needs_reauth` (same details); reconnect it first.\n\nSend an `Idempotency-Key` header to make retries safe: repeating the request with the same key and body (in the same Workspace) returns the original Post with `Idempotent-Replayed: true` instead of creating another. Reusing a key with a different body (including `media_ids` and `overrides`) returns 422 `idempotency_key_reused`. Keys are kept for at least 24 hours.\n\nFacebook publishes a video that fits its Reels specs (portrait, about 9:16, at least 540×960, 3–90 seconds) as a Reel, and other videos as Page videos; set `overrides.facebook.video_type` to `reel` or `video` to choose (`auto` is the default). Facebook fetches the video from its Media URL; the target stays `publishing` while Facebook processes it (checked every 30 seconds, giving up after 2 hours). Facebook allows 30 API-published Reels per Page per rolling 24 hours. Creating a Post checks this first: when the Page already has 30 Reels published in the 24 hours before the Post's publish time, or pending (scheduled or in progress) and due to publish within 24 hours either side of it (an overdue one counts as publishing now), the request fails with 422 `publishing_limit_reached` naming the target (`details.field`, `details.social_account_id`, plus `cap`, `limit`, `window_seconds`, `count`). The check is repeated just before publishing (Reels in progress count): a Reel over the limit then fails its target with `publishing_limit_reached` (same details).\n\nInstagram publishes in the background through Instagram's media containers: the job creates the containers, checks once a minute until Instagram has processed them (giving up after 30 minutes), then publishes. Before creating containers it checks the account's 24-hour publishing limit (`content_publishing_limit`, currently 50 posts; a carousel counts as one), counting the account's other Instagram posts still being published through Simple Socials on top of Instagram's own count; when it is used up the target fails with `publishing_limit_reached`.",
        "tags": [
          "Posts"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Any unique string per logical request, e.g. a UUID."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "social_account_ids"
                ],
                "additionalProperties": false,
                "properties": {
                  "caption": {
                    "type": "string",
                    "description": "Post text."
                  },
                  "link": {
                    "type": "string",
                    "format": "uri",
                    "description": "Absolute http(s) URL to share."
                  },
                  "media_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 50,
                    "description": "Media to attach, in order (IDs from `/v1/media`; each must be `ready`). Errors name the item as `media_ids[i]`."
                  },
                  "overrides": {
                    "type": "object",
                    "description": "Per-provider content, keyed by provider name (`facebook`, `instagram`); applied to that provider's targets. Unknown providers fail with `validation_failed` on `overrides.<provider>`.",
                    "additionalProperties": {
                      "$ref": "#/components/schemas/PostOverride"
                    },
                    "example": {
                      "instagram": {
                        "caption": "Fresh bread today! #sourdough",
                        "link": ""
                      },
                      "facebook": {
                        "video_type": "reel"
                      }
                    }
                  },
                  "social_account_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "maxItems": 50,
                    "description": "Distinct, active Social Accounts of your Workspace to publish to."
                  },
                  "publish_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time",
                    "description": "When to publish, as RFC 3339 with any timezone offset (e.g. `2026-01-16T09:30:00-05:00`). Omitted, null or in the past means now. Anything else (no offset, a number) fails with `validation_failed` on `publish_at`.",
                    "example": "2026-01-16T09:30:00-05:00"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The Post (or, for a replayed Idempotency-Key, the original Post in its current state).",
            "headers": {
              "Idempotent-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Present when the response replays an earlier request."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "422": {
            "description": "Invalid request (`validation_failed`), an Idempotency-Key reused with a different body (`idempotency_key_reused`), a target that `needs_reauth` (`account_needs_reauth`), deleted Media (`media_deleted`), or a target already at a platform publishing cap (`publishing_limit_reached`; `details.social_account_id` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/posts/{id}": {
      "get": {
        "operationId": "getPost",
        "summary": "Get a Post",
        "description": "Returns the Post with each target's status, platform post ID, permalink and error.",
        "tags": [
          "Posts"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "post_6h2kq7d2m5x9fz4ab6c8hjtn"
          }
        ],
        "responses": {
          "200": {
            "description": "The Post.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/posts/{id}/cancel": {
      "post": {
        "operationId": "cancelPost",
        "summary": "Cancel a Post",
        "description": "Cancels the Post's unpublished targets (`scheduled`, or `publishing` while queued or waiting to retry); cancelled targets never publish. Targets that already published or failed are left as they are. Send `target_ids` to cancel only some targets; omit the body (or the field) to cancel all. A target whose publish request is already in flight at the platform may still publish, and then reports `published`. Returns the Post in its new state.",
        "tags": [
          "Posts"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "post_6h2kq7d2m5x9fz4ab6c8hjtn"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "target_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Post Target IDs of this Post to cancel. An ID that isn't one of its targets fails with `validation_failed` (`details.field` is `target_ids[i]`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The Post.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/media": {
      "post": {
        "operationId": "createMediaFromURL",
        "summary": "Create Media from a URL",
        "description": "Creates `pending` Media and copies the file at `url` into storage in the background. Poll `GET /v1/media/{id}` until it is `ready` (or `failed`, with an `error`). The URL must be publicly reachable over http(s); URLs pointing at private, loopback or link-local addresses (including through DNS or redirects) are refused with `source_url_not_allowed`. The file's type is detected from its bytes, not the server's Content-Type. Failure codes: `source_fetch_failed` (HTTP error or unreachable after retries), `source_url_not_allowed`, `media_too_large`, `unsupported_media_type`, `storage_failed`.",
        "tags": [
          "Media"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "example": "https://cdn.example.com/photos/cake.jpg"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Pending Media; the copy runs in the background.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Media"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "422": {
            "description": "The URL is malformed or not allowed. Codes: `validation_failed`, `source_url_not_allowed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/media/uploads": {
      "post": {
        "operationId": "createMediaUpload",
        "summary": "Start a direct upload",
        "description": "Creates `pending` Media and returns a presigned URL. Upload the file with an HTTP `PUT` to `upload.url`, sending exactly the headers in `upload.headers` (the Content-Type is part of the signature), before `upload.expires_at` (1 hour). The bytes go straight to storage, not through the API. Then call `POST /v1/media/{id}/complete`. Supported types: `image/jpeg`, `image/png`, `image/gif`, `video/mp4`, `video/quicktime` (each platform accepts a subset when you post). `size` must not exceed the Workspace's max file size (250 MB by default); the actual upload is checked again on complete.",
        "tags": [
          "Media"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content_type",
                  "size"
                ],
                "additionalProperties": false,
                "properties": {
                  "filename": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "Optional; informational only.",
                    "example": "cake.jpg"
                  },
                  "content_type": {
                    "type": "string",
                    "example": "image/jpeg"
                  },
                  "size": {
                    "type": "integer",
                    "format": "int64",
                    "minimum": 1,
                    "description": "File size in bytes.",
                    "example": 482113
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pending Media with upload instructions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaUpload"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "422": {
            "description": "The request is invalid. Codes: `validation_failed`, `unsupported_media_type`, `media_too_large`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/media/{id}/complete": {
      "post": {
        "operationId": "completeMediaUpload",
        "summary": "Complete a direct upload",
        "description": "Checks the uploaded file and marks the Media `ready`, recording its detected type, size and, where detectable, dimensions and duration. If the file is larger than the Workspace's limit, or its bytes are not the declared type, it is deleted, the Media becomes `failed` and the error is returned. Completing `ready` Media again returns it unchanged.",
        "tags": [
          "Media"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "med_3q7kz2m5x9fz4ab6c8hjtnwe"
          }
        ],
        "responses": {
          "200": {
            "description": "The ready Media.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Media"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Nothing has been uploaded yet, or the Media is not a pending upload. Codes: `media_not_uploaded`, `media_not_pending`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The uploaded file was rejected; the Media is now `failed`. Codes: `media_too_large`, `unsupported_media_type`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/media/{id}": {
      "get": {
        "operationId": "getMedia",
        "summary": "Get Media",
        "description": "Returns the Media's status, type, size and, once `ready`, its public URL.",
        "tags": [
          "Media"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "med_3q7kz2m5x9fz4ab6c8hjtnwe"
          }
        ],
        "responses": {
          "200": {
            "description": "The Media.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Media"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "operationId": "health",
        "summary": "Health check",
        "description": "Returns 200 when the service and its database are reachable, 503 otherwise. No authentication.",
        "tags": [
          "Operations"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Healthy.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The database is unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhook-endpoints": {
      "post": {
        "operationId": "createWebhookEndpoint",
        "summary": "Register a Webhook Endpoint",
        "tags": [
          "Webhooks"
        ],
        "description": "Registers a URL to receive signed events. The response includes the endpoint's signing `secret`, shown only here and when rotated; store it to verify deliveries. A Workspace can have at most 10 endpoints.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "Where events are POSTed. Must be `https` on a public host name or address; loopback, private, link-local and other non-public addresses are refused, also when a host name resolves to one at delivery time. No credentials or fragment."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "event_types": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "post.published",
                        "post.failed",
                        "account.disconnected"
                      ]
                    },
                    "description": "Event types to receive. Default on create: all of them."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Disabled endpoints receive nothing, and their pending retries fail. Default on create: true."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registered; includes `secret`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "operationId": "listWebhookEndpoints",
        "summary": "List Webhook Endpoints",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "`next_cursor` from the previous page."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of Webhook Endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookEndpoint"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/webhook-endpoints/{id}": {
      "get": {
        "operationId": "getWebhookEndpoint",
        "summary": "Get a Webhook Endpoint",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "whe_5x9fz4ab6c8hjtn2p3kq7d2m"
          }
        ],
        "responses": {
          "200": {
            "description": "The endpoint (without its secret).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateWebhookEndpoint",
        "summary": "Update a Webhook Endpoint",
        "tags": [
          "Webhooks"
        ],
        "description": "Changes the fields present in the body; omitted fields stay as they are. `description: null` clears the description.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "whe_5x9fz4ab6c8hjtn2p3kq7d2m"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "Where events are POSTed. Must be `https` on a public host name or address; loopback, private, link-local and other non-public addresses are refused, also when a host name resolves to one at delivery time. No credentials or fragment."
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500
                  },
                  "event_types": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "post.published",
                        "post.failed",
                        "account.disconnected"
                      ]
                    },
                    "description": "Event types to receive. Default on create: all of them."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Disabled endpoints receive nothing, and their pending retries fail. Default on create: true."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhookEndpoint",
        "summary": "Delete a Webhook Endpoint",
        "tags": [
          "Webhooks"
        ],
        "description": "Removes the endpoint and its delivery history; pending retries stop.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "whe_5x9fz4ab6c8hjtn2p3kq7d2m"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/webhook-endpoints/{id}/rotate-secret": {
      "post": {
        "operationId": "rotateWebhookSecret",
        "summary": "Rotate a Webhook Endpoint's secret",
        "tags": [
          "Webhooks"
        ],
        "description": "Replaces the signing secret immediately and returns the new one (shown only here). Every delivery attempt from now on, including retries of earlier events, is signed with the new secret.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "whe_5x9fz4ab6c8hjtn2p3kq7d2m"
          }
        ],
        "responses": {
          "200": {
            "description": "The endpoint with its new `secret`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/webhook-endpoints/{id}/deliveries": {
      "get": {
        "operationId": "listWebhookDeliveries",
        "summary": "List an endpoint's deliveries",
        "tags": [
          "Webhooks"
        ],
        "description": "Every event sent (or being sent) to the endpoint, with each HTTP attempt.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "whe_5x9fz4ab6c8hjtn2p3kq7d2m"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "`next_cursor` from the previous page."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of deliveries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookDelivery"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/WorkspaceDisabled"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "webhooks": {
    "post.published": {
      "post": {
        "operationId": "postPublishedEvent",
        "summary": "A Post Target was published",
        "description": "Every delivery is a `POST` with a JSON body, signed with the endpoint's secret. To verify it: take the `Simple-Socials-Timestamp` header (Unix seconds) and the raw request body, compute HMAC-SHA256 with the secret (the whole `whsec_…` string as the key) over `timestamp + \".\" + body`, hex-encode it, and compare it in constant time with the `v1=` value in `Simple-Socials-Signature`. Reject deliveries whose timestamp is more than 5 minutes from your clock, to stop replays. Answer with any 2xx status within 10 seconds; anything else (including redirects and timeouts) is retried with exponential backoff for about 24 hours. Delivery is at least once and unordered: deduplicate by event `id`.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "Simple-Socials-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd",
            "description": "`v1=` + hex HMAC-SHA256(secret, timestamp + \".\" + body)."
          },
          {
            "name": "Simple-Socials-Timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "1768478400",
            "description": "Unix seconds when this attempt was signed."
          },
          {
            "name": "Simple-Socials-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Simple-Socials-Event-Type",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "post.published",
                "post.failed",
                "account.disconnected"
              ]
            }
          },
          {
            "name": "Simple-Socials-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PostPublishedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any other answer is retried."
          }
        }
      }
    },
    "post.failed": {
      "post": {
        "operationId": "postFailedEvent",
        "summary": "A Post Target failed",
        "description": "Every delivery is a `POST` with a JSON body, signed with the endpoint's secret. To verify it: take the `Simple-Socials-Timestamp` header (Unix seconds) and the raw request body, compute HMAC-SHA256 with the secret (the whole `whsec_…` string as the key) over `timestamp + \".\" + body`, hex-encode it, and compare it in constant time with the `v1=` value in `Simple-Socials-Signature`. Reject deliveries whose timestamp is more than 5 minutes from your clock, to stop replays. Answer with any 2xx status within 10 seconds; anything else (including redirects and timeouts) is retried with exponential backoff for about 24 hours. Delivery is at least once and unordered: deduplicate by event `id`.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "Simple-Socials-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd",
            "description": "`v1=` + hex HMAC-SHA256(secret, timestamp + \".\" + body)."
          },
          {
            "name": "Simple-Socials-Timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "1768478400",
            "description": "Unix seconds when this attempt was signed."
          },
          {
            "name": "Simple-Socials-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Simple-Socials-Event-Type",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "post.published",
                "post.failed",
                "account.disconnected"
              ]
            }
          },
          {
            "name": "Simple-Socials-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PostFailedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any other answer is retried."
          }
        }
      }
    },
    "account.disconnected": {
      "post": {
        "operationId": "accountDisconnectedEvent",
        "summary": "A Social Account was disconnected",
        "description": "Every delivery is a `POST` with a JSON body, signed with the endpoint's secret. To verify it: take the `Simple-Socials-Timestamp` header (Unix seconds) and the raw request body, compute HMAC-SHA256 with the secret (the whole `whsec_…` string as the key) over `timestamp + \".\" + body`, hex-encode it, and compare it in constant time with the `v1=` value in `Simple-Socials-Signature`. Reject deliveries whose timestamp is more than 5 minutes from your clock, to stop replays. Answer with any 2xx status within 10 seconds; anything else (including redirects and timeouts) is retried with exponential backoff for about 24 hours. Delivery is at least once and unordered: deduplicate by event `id`.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "Simple-Socials-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd",
            "description": "`v1=` + hex HMAC-SHA256(secret, timestamp + \".\" + body)."
          },
          {
            "name": "Simple-Socials-Timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "1768478400",
            "description": "Unix seconds when this attempt was signed."
          },
          {
            "name": "Simple-Socials-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Simple-Socials-Event-Type",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "post.published",
                "post.failed",
                "account.disconnected"
              ]
            }
          },
          {
            "name": "Simple-Socials-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountDisconnectedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any other answer is retried."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API Key issued by the operator, sent as `Authorization: Bearer <key>`."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The API Key is missing, invalid or revoked (code `unauthorized`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ValidationFailed": {
        "description": "The request is invalid (code `validation_failed`); `details.field` names the offending field when there is one.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such resource in your Workspace (code `not_found`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "LimitExceeded": {
        "description": "A per-Workspace limit was reached (code `limit_exceeded`); `details.limit` is the limit.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "WorkspaceDisabled": {
        "description": "The operator disabled this Workspace (code `workspace_disabled`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until a request will be accepted again.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every error response uses this shape.",
        "required": [
          "code",
          "message",
          "details"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable, machine-readable error code.",
            "enum": [
              "unauthorized",
              "not_found",
              "method_not_allowed",
              "validation_failed",
              "idempotency_key_reused",
              "limit_exceeded",
              "rate_limited",
              "workspace_disabled",
              "account_needs_reauth",
              "unavailable",
              "invalid_signed_request",
              "unsupported_media_type",
              "media_too_large",
              "media_not_uploaded",
              "media_not_pending",
              "media_deleted",
              "publishing_limit_reached",
              "source_url_not_allowed",
              "internal_error"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation."
          },
          "details": {
            "type": "object",
            "description": "Extra structured information; may be empty.",
            "additionalProperties": true
          }
        }
      },
      "Workspace": {
        "type": "object",
        "required": [
          "id",
          "name",
          "status",
          "limits",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "ws_3kq7d2m5x9fz4ab6c8hjtn2p"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ]
          },
          "limits": {
            "type": "object",
            "required": [
              "requests_per_minute",
              "max_social_accounts",
              "max_file_bytes"
            ],
            "properties": {
              "requests_per_minute": {
                "type": "integer"
              },
              "max_social_accounts": {
                "type": "integer"
              },
              "max_file_bytes": {
                "type": "integer"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ConnectLink": {
        "type": "object",
        "required": [
          "id",
          "provider",
          "url",
          "redirect_url",
          "social_account_id",
          "expires_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "cl_3kq7d2m5x9fz4ab6c8hjtn2p"
          },
          "social_account_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The Social Account this link reconnects, or null for a link that connects new accounts."
          },
          "provider": {
            "type": "string",
            "enum": [
              "facebook",
              "instagram"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The page to send to the account owner. Returned only when the link is created.",
            "example": "https://connect.simplesocials.dev/c/k3j…"
          },
          "redirect_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SocialAccount": {
        "type": "object",
        "required": [
          "id",
          "provider",
          "platform_account_id",
          "display_name",
          "username",
          "avatar_url",
          "status",
          "scopes",
          "token_expires_at",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "sa_3kq7d2m5x9fz4ab6c8hjtn2p"
          },
          "provider": {
            "type": "string",
            "enum": [
              "facebook",
              "instagram"
            ]
          },
          "platform_account_id": {
            "type": "string",
            "description": "The platform's ID for the account: the Facebook Page ID, or the Instagram professional account ID (IG user ID)."
          },
          "display_name": {
            "type": "string",
            "description": "Page or account name."
          },
          "username": {
            "type": [
              "string",
              "null"
            ]
          },
          "avatar_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "status": {
            "type": "string",
            "description": "`needs_reauth`: the platform rejected the account's token or permissions, or its token expired before it could be refreshed (Instagram tokens are refreshed automatically well before their 60-day expiry). New Posts to it are rejected with `account_needs_reauth`, and its pending (`scheduled` or `publishing`) Post Targets fail with that code at once, each firing `post.failed`, rather than at their publish time. `disconnected`: the person removed Simple Socials from their Facebook or Instagram settings; its token is gone. Either way, create a Connect Link with its `social_account_id` to reconnect it in place.",
            "enum": [
              "active",
              "needs_reauth",
              "disconnected"
            ]
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Permissions granted on the platform."
          },
          "token_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Null when the token does not expire (Facebook Page tokens). Instagram tokens are long-lived (about 60 days) and refreshed automatically."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Post": {
        "type": "object",
        "required": [
          "id",
          "caption",
          "link",
          "media_ids",
          "overrides",
          "status",
          "publish_at",
          "targets",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "post_6h2kq7d2m5x9fz4ab6c8hjtn"
          },
          "caption": {
            "type": "string"
          },
          "link": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "media_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The Post's Media, in order (empty for none)."
          },
          "overrides": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/PostOverride"
            },
            "description": "Per-provider content overrides (empty for none)."
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "publishing",
              "published",
              "failed",
              "partially_failed",
              "cancelled"
            ],
            "description": "Summary of the targets: `publishing` while any target is being published (or waiting to retry), else `scheduled` while any waits for `publish_at`; then `published` when none failed (cancelled targets aside), `failed` when none published, `partially_failed` when some published and some failed, `cancelled` when all were cancelled."
          },
          "publish_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the targets publish: the requested `publish_at`, or the creation time when it was omitted or in the past."
          },
          "targets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PostTarget"
            },
            "description": "One per Social Account, in request order."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PostOverride": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "caption": {
            "type": "string",
            "description": "Replaces the Post's caption on this provider."
          },
          "link": {
            "type": [
              "string",
              "null"
            ],
            "description": "Replaces the Post's `link` on this provider. `\"\"` or `null` removes it: Instagram can't publish links, so a Post with a `link` that also targets Instagram must set `\"instagram\": {\"link\": \"\"}`. Returned as `\"\"` when removed. Errors name `overrides.<provider>.link`."
          },
          "video_type": {
            "type": "string",
            "enum": [
              "auto",
              "reel",
              "video"
            ],
            "description": "Facebook only (`overrides.facebook.video_type`), for a Post with one video. `auto` (default): a Reel when the video fits Reels' specs (portrait, about 9:16, at least 540×960, 3–90 seconds), otherwise a Page video. `reel`: always a Reel; a video outside Reels' specs fails with `validation_failed` on `overrides.facebook.video_type`. `video`: always a Page video (doesn't count against the Reels limit). Other providers reject it."
          }
        }
      },
      "PostTarget": {
        "type": "object",
        "required": [
          "id",
          "social_account_id",
          "provider",
          "status",
          "attempts",
          "platform_post_id",
          "permalink",
          "error",
          "published_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "pt_4ab6c8hjtn2p3kq7d2m5x9fz"
          },
          "social_account_id": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "enum": [
              "facebook",
              "instagram"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "publishing",
              "published",
              "failed",
              "cancelled"
            ],
            "description": "`scheduled` waits for the Post's `publish_at`. `publishing` covers queued, in-progress, waiting for the platform to process media, and waiting-to-retry. `cancelled` never publishes. Transient platform errors (rate limits, timeouts, 5xx) are retried with exponential backoff; permanent ones fail at once."
          },
          "attempts": {
            "type": "integer",
            "description": "Publish attempts made so far (waiting for the platform to process media is not an attempt)."
          },
          "platform_post_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The platform's post ID once published (Facebook: `{page-id}_{post-id}`; Instagram: the IG Media ID)."
          },
          "permalink": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "error": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PostTargetError"
              },
              {
                "type": "null"
              }
            ],
            "description": "Set when the target failed."
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "PostTargetError": {
        "type": "object",
        "required": [
          "code",
          "message",
          "details"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "platform_error",
              "account_needs_reauth",
              "account_disconnected",
              "publishing_limit_reached"
            ],
            "description": "`publishing_limit_reached`: the account has used its platform's 24-hour publishing limit: Instagram's limit of API-published posts (details: `quota_usage`, `quota_total`, `in_flight` (the account's other posts being published, which Instagram doesn't count yet), and the platform error when Instagram reported it at publish time), or Facebook's 30 Reels per Page (details: `cap`, `limit`, `window_seconds`, `count`); create the Post again later. `platform_error`: the platform rejected the post, or kept failing transiently until retries ran out. `account_needs_reauth`: the platform rejected the account's token or permissions (while publishing this or another target, or refreshing the token), or the token expired; the Social Account is now `needs_reauth` and must be reconnected. Every pending target of the account fails with this code as soon as that happens. `account_disconnected`: the Social Account was deleted or disconnected (the platform user removed the app or asked for data deletion) before publishing."
          },
          "message": {
            "type": "string"
          },
          "details": {
            "type": "object",
            "additionalProperties": true,
            "description": "For platform errors: `provider`, `retryable`, `http_status`, `platform_code`, `platform_subcode`, `platform_type`, `trace_id` and the raw `platform_error` body. When Instagram fails to process media: `container_id`, `container_status` and `platform_subcode` when known."
          }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "required": [
          "id",
          "url",
          "description",
          "event_types",
          "enabled",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "whe_5x9fz4ab6c8hjtn2p3kq7d2m"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "event_types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "post.published",
                "post.failed",
                "account.disconnected"
              ]
            }
          },
          "enabled": {
            "type": "boolean"
          },
          "secret": {
            "type": "string",
            "example": "whsec_0000000000000000000000000000000000000000000000000000000000000000",
            "description": "The signing secret. Present only in the create and rotate-secret responses."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "required": [
          "id",
          "event_id",
          "event_type",
          "status",
          "last_response_code",
          "last_error",
          "delivered_at",
          "attempts",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "whd_c8hjtn2p3kq7d2m5x9fz4ab6",
            "description": "Also sent as the `Simple-Socials-Delivery-Id` header."
          },
          "event_id": {
            "type": "string"
          },
          "event_type": {
            "type": "string",
            "enum": [
              "post.published",
              "post.failed",
              "account.disconnected"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "succeeded",
              "failed"
            ],
            "description": "`pending` covers queued and waiting-to-retry. `failed`: retries ran out (about 24 hours after the event) or the endpoint was disabled."
          },
          "last_response_code": {
            "type": [
              "integer",
              "null"
            ]
          },
          "last_error": {
            "type": [
              "string",
              "null"
            ]
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "attempts": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "attempt",
                "response_code",
                "error",
                "duration_ms",
                "attempted_at"
              ],
              "properties": {
                "attempt": {
                  "type": "integer"
                },
                "response_code": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "Null when no response arrived (timeout, connection error, refused address)."
                },
                "error": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "duration_ms": {
                  "type": "integer"
                },
                "attempted_at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PostTargetEventData": {
        "type": "object",
        "required": [
          "post_id",
          "target"
        ],
        "properties": {
          "post_id": {
            "type": "string"
          },
          "target": {
            "$ref": "#/components/schemas/PostTarget"
          }
        }
      },
      "AccountDisconnectedEventData": {
        "type": "object",
        "required": [
          "social_account",
          "reason"
        ],
        "properties": {
          "social_account": {
            "type": "object",
            "required": [
              "id",
              "provider",
              "platform_account_id",
              "display_name",
              "username",
              "status"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "provider": {
                "type": "string",
                "enum": [
                  "facebook",
                  "instagram"
                ]
              },
              "platform_account_id": {
                "type": "string"
              },
              "display_name": {
                "type": "string"
              },
              "username": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "status": {
                "type": "string",
                "enum": [
                  "needs_reauth",
                  "disconnected"
                ]
              }
            }
          },
          "reason": {
            "type": "string",
            "enum": [
              "deauthorized",
              "data_deleted",
              "token_rejected",
              "token_expired"
            ],
            "description": "`deauthorized`: the person removed Simple Socials in their Facebook or Instagram settings. `data_deleted`: they asked Meta to delete their data; the account is gone. `token_rejected`: the platform rejected the token (while publishing or refreshing it); the account is `needs_reauth`. `token_expired`: the token expired before it could be refreshed; the account is `needs_reauth`. For both, send a reconnect Connect Link (`social_account_id`)."
          }
        }
      },
      "PostPublishedEvent": {
        "type": "object",
        "description": "A Post Target was published (`data.target.status` is `published`).",
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "evt_9fz4ab6c8hjtn2p3kq7d2m5x",
            "description": "Unique per event and the same on every retry and endpoint; use it to deduplicate. Also sent as `Simple-Socials-Event-Id`."
          },
          "type": {
            "type": "string",
            "const": "post.published"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "$ref": "#/components/schemas/PostTargetEventData"
          }
        }
      },
      "PostFailedEvent": {
        "type": "object",
        "description": "A Post Target failed for good (`data.target.error` says why).",
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "evt_9fz4ab6c8hjtn2p3kq7d2m5x",
            "description": "Unique per event and the same on every retry and endpoint; use it to deduplicate. Also sent as `Simple-Socials-Event-Id`."
          },
          "type": {
            "type": "string",
            "const": "post.failed"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "$ref": "#/components/schemas/PostTargetEventData"
          }
        }
      },
      "AccountDisconnectedEvent": {
        "type": "object",
        "description": "A Social Account can no longer be posted to.",
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "evt_9fz4ab6c8hjtn2p3kq7d2m5x",
            "description": "Unique per event and the same on every retry and endpoint; use it to deduplicate. Also sent as `Simple-Socials-Event-Id`."
          },
          "type": {
            "type": "string",
            "const": "account.disconnected"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "$ref": "#/components/schemas/AccountDisconnectedEventData"
          }
        }
      },
      "Media": {
        "type": "object",
        "description": "A file stored for use in Posts, served from `media.simplesocials.dev`.",
        "required": [
          "id",
          "source",
          "source_url",
          "filename",
          "status",
          "type",
          "content_type",
          "size",
          "width",
          "height",
          "duration_ms",
          "url",
          "error",
          "deleted_at",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "med_3q7kz2m5x9fz4ab6c8hjtnwe"
          },
          "source": {
            "type": "string",
            "enum": [
              "upload",
              "url"
            ]
          },
          "source_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The URL copied from (source `url`)."
          },
          "filename": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "failed",
              "deleted"
            ],
            "description": "`deleted`: cleaned up and its file removed; Posts can no longer use it (`media_deleted`). Media is deleted 7 days after every Post using it has finished (published, failed or cancelled; never while a Post using it is scheduled or publishing), 7 days after creation when never attached to a Post, and 24 hours after creation when an upload was never completed."
          },
          "type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "image",
              "video",
              null
            ]
          },
          "content_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "Declared type while a direct upload is pending; detected from the file once ready.",
            "example": "image/jpeg"
          },
          "size": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Bytes. Declared size while a direct upload is pending."
          },
          "width": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Pixels, when detectable (display orientation for videos)."
          },
          "height": {
            "type": [
              "integer",
              "null"
            ]
          },
          "duration_ms": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Videos, when detectable."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Public URL, once ready.",
            "example": "https://media.simplesocials.dev/m/med_3q7kz2m5x9fz4ab6c8hjtnwe.jpg"
          },
          "error": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": [
                  "code",
                  "message"
                ],
                "description": "Why the Media failed.",
                "properties": {
                  "code": {
                    "type": "string",
                    "enum": [
                      "unsupported_media_type",
                      "media_too_large",
                      "source_url_not_allowed",
                      "source_fetch_failed",
                      "storage_failed"
                    ]
                  },
                  "message": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "deleted_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the Media was deleted (status `deleted`)."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MediaUpload": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Media"
          },
          {
            "type": "object",
            "required": [
              "upload"
            ],
            "properties": {
              "upload": {
                "type": "object",
                "required": [
                  "url",
                  "method",
                  "headers",
                  "expires_at"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Presigned storage URL (not on the API host)."
                  },
                  "method": {
                    "type": "string",
                    "enum": [
                      "PUT"
                    ]
                  },
                  "headers": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Send exactly these headers with the PUT.",
                    "example": {
                      "Content-Type": "image/jpeg"
                    }
                  },
                  "expires_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        ]
      }
    }
  }
}
