{
  "openapi": "3.1.0",
  "info": {
    "title": "Ravenpost API",
    "version": "1.0.0",
    "description": "Schedule and publish social posts across Instagram, TikTok, X, Telegram, LinkedIn, Threads, Bluesky, YouTube and Pinterest. Every endpoint delegates to the same service the Ravenpost dashboard uses. Authenticate with a personal access token from Settings → API tokens; the token carries one workspace, which is why no route takes a workspace id.",
    "contact": {
      "name": "Ravenpost support",
      "email": "support@ravenpo.st"
    }
  },
  "servers": [
    {
      "url": "https://api.ravenpo.st"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "rvp_…"
      }
    }
  },
  "paths": {
    "/v1/me": {
      "get": {
        "summary": "The token’s workspace, its plan limits, current usage and account counts.",
        "description": "The cheapest call to verify a token works. No endpoint takes a workspace id — the token carries exactly one, so a token can never reach another tenant’s data whatever the caller sends. The month in `usage` starts on the day the plan renews (the 1st on Free); `renewsAt` is when it starts over. X is counted in credits — a post without a link is 1, one with a link about 13.3, each picture or video 1 more — and `xCreditsExtra` are credits bought on the website, spent once the month’s run out. `monthlyXPosts` and `xPostsThisMonth`, the names from before credits, still carry the same two numbers; read the credit names in new code.",
        "tags": [
          "Workspace"
        ],
        "operationId": "getMe",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "workspaceId": "wsp_...",
                  "plan": {
                    "id": "growth",
                    "name": "Growth",
                    "limits": {
                      "socialAccounts": 15,
                      "seats": 3,
                      "monthlyPosts": -1,
                      "monthlyXCredits": 350,
                      "aiCredits": 1000
                    }
                  },
                  "usage": {
                    "accounts": 6,
                    "postsThisMonth": 84,
                    "xCreditsThisMonth": 57.33,
                    "xCreditsCarried": 0,
                    "xCreditsExtra": 200,
                    "aiCreditsThisMonth": 212.4,
                    "periodStart": "2026-11-13T09:21:00.000Z",
                    "renewsAt": "2026-12-13T09:21:00.000Z"
                  },
                  "stats": {
                    "accounts": 6,
                    "scheduled": 12
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/accounts": {
      "get": {
        "summary": "Connected social accounts — id, platform, username, status.",
        "description": "Access tokens are never returned by any endpoint.",
        "tags": [
          "Accounts"
        ],
        "operationId": "getAccounts",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": "acc_...",
                    "platform": "INSTAGRAM",
                    "username": "ravenpost",
                    "status": "ACTIVE",
                    "connection": "FACEBOOK_LOGIN"
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/posts": {
      "get": {
        "summary": "List posts, newest first.",
        "description": "Without `page` this returns a plain array, matching the dashboard’s contract; with `page` it returns a paginated envelope. Automations should always pass `page` — a workspace with thousands of posts otherwise returns all of them in one response.",
        "tags": [
          "Posts"
        ],
        "operationId": "getPosts",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by status.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Only posts targeting this platform.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Filter by post format.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Caption search.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort order.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number — switches the response to an envelope.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      },
      "post": {
        "summary": "Create a post and publish, schedule, queue or save it as a draft.",
        "description": "One post, many destinations: each account in `accountIds` becomes a target with its own status, permalink and error. Caption length is validated against the tightest selected platform.",
        "tags": [
          "Posts"
        ],
        "operationId": "postPosts",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "description": "Post format. Instagram's model, reused as the shared one — a platform that doesn't honour the format falls back to its own default rather than failing."
                  },
                  "accountIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Destination accounts, from GET /v1/accounts. One post fans out to all of them — at most one X account, since X forbids the same post on several accounts."
                  },
                  "mediaIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Media assets to attach. Pass [] for a text-only post where the platform allows it."
                  },
                  "action": {
                    "type": "string",
                    "description": "What to do with it. 'schedule' needs scheduledAt; 'queue' resolves the next free slot in the workspace posting schedule and fails if none is configured."
                  },
                  "caption": {
                    "type": "string",
                    "description": "Base caption, used by every platform without an override. The text X publishes may carry one $cashtag; a second is rejected, as X would."
                  },
                  "captionOverrides": {
                    "type": "object",
                    "description": "Per-platform caption. A platform with no entry publishes the base caption. Keys are platform ids from GET /v1/platforms."
                  },
                  "scheduledAt": {
                    "type": "string",
                    "description": "When to publish, for action: 'schedule'."
                  },
                  "threadParts": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Follow-up posts published in order after the head, on platforms that declare a thread model (X, Bluesky, Mastodon, Threads, Telegram, Discord). Over the platform limit is rejected, never truncated."
                  },
                  "firstComment": {
                    "type": "string",
                    "description": "Posted as a comment under the Instagram post right after it publishes, typically for hashtags. Instagram only, and never on a story. A comment Instagram refuses does not fail the post. On update, null clears it."
                  },
                  "replyToTweetId": {
                    "type": "string",
                    "description": "Publish the head post as a reply under this tweet. Accepts an id or a tweet URL. X only — and X only takes a reply from an app to the account's own post, or to a post that @mentions it or quotes one of its posts."
                  },
                  "quoteTweetId": {
                    "type": "string",
                    "description": "Embed this tweet as a quote card. Id or URL. X only, independent of replyToTweetId — and X only takes a quote of the account's own post, or of one that @mentions it."
                  },
                  "tiktokOptions": {
                    "type": "object",
                    "description": "TikTok privacy level, interaction switches and commercial disclosure, keyed by account id. Omitted, a TikTok target publishes SELF_ONLY — TikTok requires the creator to choose, so we never pick a wider audience for them."
                  },
                  "audio": {
                    "type": "object",
                    "description": "Reel audio ({ audioId, audioVolume, videoVolume }). Instagram reels only, and only on an account connected through Facebook."
                  },
                  "reelCover": {
                    "type": "object",
                    "description": "An Instagram reel's cover: { mediaId } — a JPEG of up to 8 MB from the media library — or { offsetMs }, a frame of the video in milliseconds, before it ends. One or the other. Instagram reels only, on either connection; dropped for any other format. On update, null goes back to Instagram's default, the first frame."
                  },
                  "trialReel": {
                    "type": "string",
                    "description": "Publish an Instagram reel as a trial: shown to non-followers first, and to followers once it graduates — by hand in the Instagram app (MANUAL) or by Instagram when it performs well (SS_PERFORMANCE). Instagram decides which accounts may post one; a refused trial fails rather than going out as a normal reel. On update, \"\" or null makes it a normal reel."
                  },
                  "pinterestBoards": {
                    "type": "object",
                    "description": "Which board each Pinterest account pins to, keyed by account id (see GET /v1/accounts/:id/boards). Required to publish: a pin belongs to a board and Pinterest has no default, so a Pinterest target without one fails at publish time. A draft saves fine without it."
                  },
                  "pinterestOptions": {
                    "type": "object",
                    "description": "Pinterest pin title ({ title }), max 100 characters. Separate from the caption, which becomes the 800-character description; unset, the caption’s first line is used."
                  },
                  "linkUrl": {
                    "type": "string",
                    "description": "The URL this post points at. Facebook publishes it as a link preview card; Pinterest makes it the pin’s destination. Every other platform ignores it."
                  },
                  "recurrence": {
                    "type": "object",
                    "description": "Makes this an evergreen post: each occurrence is created after the previous one publishes. Not with an X account, whose rules forbid repeating a post."
                  }
                },
                "required": [
                  "type",
                  "accountIds",
                  "mediaIds",
                  "action"
                ],
                "additionalProperties": true
              },
              "example": {
                "type": "IMAGE",
                "caption": "Shipping today.",
                "captionOverrides": {
                  "TWITTER": "Shipping today 🚀"
                },
                "mediaIds": [
                  "med_..."
                ],
                "accountIds": [
                  "acc_...",
                  "acc_..."
                ],
                "action": "schedule",
                "scheduledAt": "2026-08-04T09:00:00.000Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/accounts/{id}/boards": {
      "get": {
        "summary": "The boards on a connected Pinterest account.",
        "description": "The only per-platform route here, and Pinterest earns it: every pin has to name a board, there is no default, and a board id appears nowhere else a caller can read it — not even in the board’s own URL, which uses a slug. Pass the ids to `pinterestBoards` on a post.",
        "tags": [
          "Accounts"
        ],
        "operationId": "getAccountsIdBoards",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A Pinterest account id from GET /v1/accounts.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "boards": [
                    {
                      "id": "1234567890",
                      "name": "Product shots",
                      "privacy": "PUBLIC"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/posts/bulk": {
      "post": {
        "summary": "Create many posts from a CSV.",
        "description": "Returns a per-line report so a caller can retry only the rows that failed, rather than guessing which half of the batch landed.",
        "tags": [
          "Posts"
        ],
        "operationId": "postPostsBulk",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "csv": {
                    "type": "string",
                    "description": "The CSV document itself, as a string."
                  }
                },
                "required": [
                  "csv"
                ],
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (10 requests/minute)"
          }
        }
      }
    },
    "/v1/posts/{id}": {
      "get": {
        "summary": "One post with every target’s status, permalink and error.",
        "tags": [
          "Posts"
        ],
        "operationId": "getPostsId",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Post id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      },
      "patch": {
        "summary": "Edit a draft or scheduled post.",
        "description": "Editing resets the post to draft — pass `action` in the same call to re-schedule or publish it. A published post cannot be edited.",
        "tags": [
          "Posts"
        ],
        "operationId": "patchPostsId",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Post id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "description": "Post format. Instagram's model, reused as the shared one — a platform that doesn't honour the format falls back to its own default rather than failing."
                  },
                  "accountIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Destination accounts, from GET /v1/accounts. One post fans out to all of them — at most one X account, since X forbids the same post on several accounts."
                  },
                  "mediaIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Media assets to attach. Pass [] for a text-only post where the platform allows it."
                  },
                  "action": {
                    "type": "string",
                    "description": "What to do with it. 'schedule' needs scheduledAt; 'queue' resolves the next free slot in the workspace posting schedule and fails if none is configured."
                  },
                  "caption": {
                    "type": "string",
                    "description": "Base caption, used by every platform without an override. The text X publishes may carry one $cashtag; a second is rejected, as X would."
                  },
                  "captionOverrides": {
                    "type": "object",
                    "description": "Per-platform caption. A platform with no entry publishes the base caption. Keys are platform ids from GET /v1/platforms."
                  },
                  "scheduledAt": {
                    "type": "string",
                    "description": "When to publish, for action: 'schedule'."
                  },
                  "threadParts": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Follow-up posts published in order after the head, on platforms that declare a thread model (X, Bluesky, Mastodon, Threads, Telegram, Discord). Over the platform limit is rejected, never truncated."
                  },
                  "firstComment": {
                    "type": "string",
                    "description": "Posted as a comment under the Instagram post right after it publishes, typically for hashtags. Instagram only, and never on a story. A comment Instagram refuses does not fail the post. On update, null clears it."
                  },
                  "replyToTweetId": {
                    "type": "string",
                    "description": "Publish the head post as a reply under this tweet. Accepts an id or a tweet URL. X only — and X only takes a reply from an app to the account's own post, or to a post that @mentions it or quotes one of its posts."
                  },
                  "quoteTweetId": {
                    "type": "string",
                    "description": "Embed this tweet as a quote card. Id or URL. X only, independent of replyToTweetId — and X only takes a quote of the account's own post, or of one that @mentions it."
                  },
                  "tiktokOptions": {
                    "type": "object",
                    "description": "TikTok privacy level, interaction switches and commercial disclosure, keyed by account id. Omitted, a TikTok target publishes SELF_ONLY — TikTok requires the creator to choose, so we never pick a wider audience for them."
                  },
                  "audio": {
                    "type": "object",
                    "description": "Reel audio ({ audioId, audioVolume, videoVolume }). Instagram reels only, and only on an account connected through Facebook."
                  },
                  "reelCover": {
                    "type": "object",
                    "description": "An Instagram reel's cover: { mediaId } — a JPEG of up to 8 MB from the media library — or { offsetMs }, a frame of the video in milliseconds, before it ends. One or the other. Instagram reels only, on either connection; dropped for any other format. On update, null goes back to Instagram's default, the first frame."
                  },
                  "trialReel": {
                    "type": "string",
                    "description": "Publish an Instagram reel as a trial: shown to non-followers first, and to followers once it graduates — by hand in the Instagram app (MANUAL) or by Instagram when it performs well (SS_PERFORMANCE). Instagram decides which accounts may post one; a refused trial fails rather than going out as a normal reel. On update, \"\" or null makes it a normal reel."
                  },
                  "pinterestBoards": {
                    "type": "object",
                    "description": "Which board each Pinterest account pins to, keyed by account id (see GET /v1/accounts/:id/boards). Required to publish: a pin belongs to a board and Pinterest has no default, so a Pinterest target without one fails at publish time. A draft saves fine without it."
                  },
                  "pinterestOptions": {
                    "type": "object",
                    "description": "Pinterest pin title ({ title }), max 100 characters. Separate from the caption, which becomes the 800-character description; unset, the caption’s first line is used."
                  },
                  "linkUrl": {
                    "type": "string",
                    "description": "The URL this post points at. Facebook publishes it as a link preview card; Pinterest makes it the pin’s destination. Every other platform ignores it."
                  },
                  "recurrence": {
                    "type": "object",
                    "description": "Makes this an evergreen post: each occurrence is created after the previous one publishes. Not with an X account, whose rules forbid repeating a post."
                  }
                },
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      },
      "delete": {
        "summary": "Delete the Ravenpost copy and cancel any pending publish.",
        "description": "This does not take down anything already published. Removing the post on the platform is a separate, opt-in action in the dashboard, and it is not available on Instagram, Threads and TikTok, which expose no delete endpoint, or on YouTube, whose delete endpoint needs a broader permission than Ravenpost holds.",
        "tags": [
          "Posts"
        ],
        "operationId": "deletePostsId",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Post id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/posts/{id}/schedule": {
      "post": {
        "summary": "Move a post to a new time.",
        "tags": [
          "Posts"
        ],
        "operationId": "postPostsIdSchedule",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Post id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scheduledAt": {
                    "type": "string",
                    "description": "New publish time."
                  }
                },
                "required": [
                  "scheduledAt"
                ],
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/posts/{id}/publish": {
      "post": {
        "summary": "Publish a post immediately, whatever it was scheduled for.",
        "tags": [
          "Posts"
        ],
        "operationId": "postPostsIdPublish",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Post id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/posts/{id}/retry": {
      "post": {
        "summary": "Re-enqueue only the failed targets.",
        "description": "Targets that already published are left alone, so a retry can’t double-post.",
        "tags": [
          "Posts"
        ],
        "operationId": "postPostsIdRetry",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Post id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/media": {
      "get": {
        "summary": "The workspace media library.",
        "tags": [
          "Media"
        ],
        "operationId": "getMedia",
        "parameters": [
          {
            "name": "folderId",
            "in": "query",
            "required": false,
            "description": "Limit to one folder.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      },
      "post": {
        "summary": "Step 2 of an upload: register the uploaded object as an asset.",
        "description": "Returns the asset `id` to put in `mediaIds`.",
        "tags": [
          "Media"
        ],
        "operationId": "postMedia",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "description": "Asset kind."
                  },
                  "storageKey": {
                    "type": "string",
                    "description": "From the presign response."
                  },
                  "url": {
                    "type": "string",
                    "description": "The `publicUrl` from the presign response, unchanged — any other address is refused."
                  },
                  "mimeType": {
                    "type": "string",
                    "description": "Recorded for the per-platform compatibility check."
                  },
                  "width": {
                    "type": "number",
                    "description": "Pixels."
                  },
                  "height": {
                    "type": "number",
                    "description": "Pixels."
                  },
                  "durationSec": {
                    "type": "number",
                    "description": "Video length."
                  },
                  "sizeBytes": {
                    "type": "number",
                    "description": "File size, checked against each platform’s cap."
                  }
                },
                "required": [
                  "type",
                  "storageKey",
                  "url"
                ],
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/media/uploads": {
      "post": {
        "summary": "Step 1 of an upload: get a presigned PUT URL.",
        "description": "The client sends the bytes straight to object storage, so a large video never travels through the API — and never has to be base64-encoded into a request. PUT them to `uploadUrl` within ten minutes, with the same `Content-Type` as `contentType`, then register them with POST /v1/media.",
        "tags": [
          "Media"
        ],
        "operationId": "postMediaUploads",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "filename": {
                    "type": "string",
                    "description": "Original filename, used for a readable storage key."
                  },
                  "contentType": {
                    "type": "string",
                    "description": "MIME type of the bytes you are about to PUT: an image (JPEG, PNG, WebP, GIF, HEIC, AVIF) or a video (MP4, MOV, WebM). Anything else is refused."
                  }
                },
                "required": [
                  "filename",
                  "contentType"
                ],
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "uploadUrl": "https://...",
                  "storageKey": "...",
                  "publicUrl": "https://cdn..."
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (60 requests/minute)"
          }
        }
      }
    },
    "/v1/media/{id}/variants": {
      "post": {
        "summary": "Reshape one image into several platform canvases in a single call.",
        "description": "Cropping is saliency-based by default — an off-centre subject survives a 4:5 → 9:16 cut, where a centre crop would keep the wall behind them. Pass `crop: \"centre\"` for posters, screenshots and flat graphics composed around their middle. Two formats with the same dimensions produce one file, not two, and each variant inherits the source’s alt text and folder. Images only.",
        "tags": [
          "Media"
        ],
        "operationId": "postMediaIdVariants",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Source image asset id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "formats": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Canvases to produce: 1080×1350 (4:5), 1080×1080 (1:1), 1080×566 (1.91:1), 1080×1920 (9:16 — also reels and TikTok photos)."
                  },
                  "fit": {
                    "type": "string",
                    "description": "'cover' crops to fill (default); 'contain' pads so nothing is cut off."
                  },
                  "crop": {
                    "type": "string",
                    "description": "'smart' is saliency-based and the default; 'centre' is the predictable middle crop."
                  }
                },
                "required": [
                  "formats"
                ],
                "additionalProperties": true
              },
              "example": {
                "formats": [
                  "feed_portrait",
                  "story",
                  "landscape"
                ],
                "crop": "smart"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "sourceId": "med_b2",
                  "variants": [
                    {
                      "id": "med_c1",
                      "url": "https://cdn...",
                      "format": "feed_portrait",
                      "width": 1080,
                      "height": 1350
                    },
                    {
                      "id": "med_c2",
                      "url": "https://cdn...",
                      "format": "story",
                      "width": 1080,
                      "height": 1920
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (30 requests/minute)"
          }
        }
      }
    },
    "/v1/analytics": {
      "get": {
        "summary": "Followers per account, a 30-day daily series, and engagement on recent posts.",
        "description": "A `followers` of `null` means we hold none for the platform — LinkedIn, Threads and Discord expose no insights, TikTok’s need a scope that is not approved yet, and X’s are not read because X charges for every API read. Null means unknown, never zero; an absent post metric was not collected rather than being 0.",
        "tags": [
          "Analytics"
        ],
        "operationId": "getAnalytics",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/best-times": {
      "get": {
        "summary": "Recommended posting hours, measured from this workspace’s own posts.",
        "description": "Overall and per platform, in the workspace timezone. `score` is a multiple of a typical post for that account, so 1.8 means “roughly 80% better than normal”. When `insufficient` is true `slots` is empty — that is “unknown”, not “no good time”. `reason` says which wall it hit: `few_posts` / `few_samples` (more publishing fixes it — `observations` vs `minObservations` says how far off), `low_engagement` (posts earn too few interactions for any hour to stand out), `not_collected` (the platform reports no per-post engagement at all).",
        "tags": [
          "Analytics"
        ],
        "operationId": "getBestTimes",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "overall": {
                    "timezone": "Europe/Istanbul",
                    "observations": 46,
                    "insufficient": false,
                    "minObservations": 12,
                    "reason": "ok",
                    "slots": [
                      {
                        "weekday": 2,
                        "minutes": 1140,
                        "score": 1.84,
                        "samples": 5
                      }
                    ],
                    "grid": [
                      [
                        null,
                        null,
                        0.9,
                        "..."
                      ]
                    ]
                  },
                  "byPlatform": [
                    {
                      "platform": "INSTAGRAM",
                      "report": {
                        "...": "..."
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/emails": {
      "post": {
        "summary": "Send one transactional email — a template turned on in Email → Transactional, to one address, filled with your values.",
        "description": "Queued, not sent in the request: the answer (202) is the email with `status: \"QUEUED\"`, and `GET /v1/emails/:id` says what became of it. A template turned off in Email → Transactional is answered 409 `template_disabled` and nothing is sent — the request is kept as a skipped email, so keep calling: once it is turned on again, the same request sends. Every tag the template writes without a fallback has to be in `data`, or the request is refused (400, `missing_data`) — a hole in a sentence is never sent. An `info` template carries an unsubscribe link and is not sent to anybody who unsubscribed from the workspace’s emails; a `transactional` one (receipts, resets) has none and is; neither goes to an address that bounced or marked the workspace’s mail as spam. Pass an `idempotencyKey` and a retried request gets the first one’s email back (`duplicate: true`) instead of a second email. Needs Email with transactional emails on the workspace’s plan; through Ravenpost’s own sending it counts against the month’s emails (429 `email_month_spent` when they are used up).",
        "tags": [
          "Email"
        ],
        "operationId": "postEmails",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "template": {
                    "type": "string",
                    "description": "The template’s key, e.g. `welcome`."
                  },
                  "to": {
                    "type": "string",
                    "description": "One email address."
                  },
                  "data": {
                    "type": "object",
                    "description": "The values its tags take: text, numbers or true/false, up to 50 keys. `email`, `sender_name` and `unsubscribe_url` are filled by Ravenpost."
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "description": "Up to 200 characters. A second request with the same key in this workspace sends nothing and answers the first email."
                  }
                },
                "required": [
                  "template",
                  "to"
                ],
                "additionalProperties": true
              },
              "example": {
                "template": "welcome",
                "to": "ada@example.com",
                "data": {
                  "first_name": "Ada"
                },
                "idempotencyKey": "welcome-user-123"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "em_k3",
                  "templateId": "tpl_w1",
                  "template": "welcome",
                  "kind": "info",
                  "to": "ada@example.com",
                  "status": "QUEUED",
                  "source": "api",
                  "error": null,
                  "createdAt": "2026-11-02T12:00:00.000Z",
                  "duplicate": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (600 requests/minute)"
          }
        }
      }
    },
    "/v1/emails/{id}": {
      "get": {
        "summary": "What became of one transactional email: queued, sent, delivered, opened, clicked, bounced — or why it did not go.",
        "description": "`status` is QUEUED, SENDING, SENT, DELIVERED, BOUNCED, COMPLAINED, FAILED or SKIPPED; `error` says why one failed or was skipped (the address unsubscribed, bounced before, a domain that takes no mail). Opens and clicks are timestamps beside the status. Kept for 90 days.",
        "tags": [
          "Email"
        ],
        "operationId": "getEmailsId",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The id `POST /v1/emails` answered.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "id": "em_k3",
                  "template": "welcome",
                  "to": "ada@example.com",
                  "status": "DELIVERED",
                  "sentAt": "2026-11-02T12:00:02.000Z",
                  "deliveredAt": "2026-11-02T12:00:04.000Z",
                  "openedAt": null,
                  "error": null
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/email-templates": {
      "get": {
        "summary": "The transactional templates that are turned on, each with the values it takes and the ones it cannot go out without.",
        "description": "`requiredData` lists the keys `data` must carry; every other tag has a fallback written in the template.",
        "tags": [
          "Email"
        ],
        "operationId": "getEmailTemplates",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "templates": [
                    {
                      "key": "trial-started",
                      "name": "Trial started",
                      "kind": "transactional",
                      "tags": [
                        {
                          "key": "plan_name",
                          "fallback": null,
                          "required": true
                        }
                      ],
                      "requiredData": [
                        "plan_name",
                        "trial_end"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/queue-slots": {
      "get": {
        "summary": "The weekly posting schedule that `action: \"queue\"` publishes into.",
        "description": "Workspace-wide and shared by every connected account, in the workspace timezone. weekday 0 = Sunday.",
        "tags": [
          "Reference"
        ],
        "operationId": "getQueueSlots",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    },
    "/v1/platforms": {
      "get": {
        "summary": "Per-platform caption budgets, media rules, formats, thread caps and recommended specs.",
        "description": "Read this instead of hardcoding limits: they change when a platform changes, and this endpoint is generated from the same registry the publisher enforces.",
        "tags": [
          "Reference"
        ],
        "operationId": "getPlatforms",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success"
          },
          "401": {
            "description": "Missing, malformed or revoked token"
          },
          "429": {
            "description": "Rate limited (120 requests/minute)"
          }
        }
      }
    }
  }
}