{
  "openapi": "3.1.0",
  "info": {
    "title": "sprkly API",
    "version": "1.0.0",
    "description": "Schedule and publish social media posts (Instagram, TikTok, YouTube Shorts, Threads, Facebook) programmatically. Authenticate with a Bearer API key created in Settings.",
    "contact": {
      "email": "support@sprkly.app",
      "url": "https://sprkly.app/docs/api"
    }
  },
  "servers": [
    {
      "url": "https://sprkly.app"
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "sprkly API key created in Settings, sent as `Authorization: Bearer <key>`. Scopes: post:write, post:read, media:write, account:read."
      }
    },
    "schemas": {
      "CreatePostRequest": {
        "type": "object",
        "required": [
          "platforms",
          "profileIds"
        ],
        "properties": {
          "platforms": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "instagram",
                "tiktok",
                "youtube",
                "threads",
                "facebook"
              ]
            }
          },
          "profileIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Connected account IDs from GET /api/v1/connections."
          },
          "caption": {
            "type": "string"
          },
          "title": {
            "type": "string",
            "description": "Required for YouTube and TikTok."
          },
          "mediaUrls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "Publicly reachable URLs, in slide order. Usable for every platform: Instagram and Threads fetch them directly, and for TikTok, YouTube and Facebook sprkly downloads the file into its own storage during this request, so a bad link is a 422 here rather than a publish-time failure later. Google Drive and Dropbox share links are converted automatically. 50 MB per file. Use mediaIds instead when the file is already in sprkly or is going on several posts."
          },
          "mediaId": {
            "type": "string",
            "description": "A single id returned from POST /api/v1/media. Shorthand for a one-item mediaIds."
          },
          "mediaIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ids from POST /api/v1/media, in slide order — the carousel and photo-set path. Array order is the published order. Instagram takes at most 10 slides; TikTok photo sets take up to 35 and every image must be the same pixel size (mismatches are padded automatically)."
          },
          "scheduledTime": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601. Omit to publish as soon as possible."
          },
          "externalId": {
            "type": "string",
            "description": "Caller-supplied idempotency key; prevents duplicate posts on retry."
          },
          "platformMeta": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "CreatePostResponse": {
        "type": "object",
        "properties": {
          "postIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Post": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "posted",
              "failed"
            ]
          },
          "failedReason": {
            "type": "string",
            "nullable": true
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "permalink": {
            "type": "string",
            "nullable": true
          }
        }
      }
    }
  },
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "summary": "Service health",
        "security": [],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/connections": {
      "get": {
        "summary": "List connected social accounts",
        "description": "Returns the caller’s connected accounts and their profile IDs (scope: account:read).",
        "responses": {
          "200": {
            "description": "Connected accounts"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/api/v1/posts": {
      "post": {
        "summary": "Create / schedule a post",
        "description": "Creates a scheduled post; sprkly publishes it at scheduledTime (scope: post:write).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePostRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatePostResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/api/v1/posts/schedule": {
      "post": {
        "summary": "Schedule a post (explicit schedule endpoint)",
        "responses": {
          "200": {
            "description": "Scheduled"
          }
        }
      }
    },
    "/api/v1/posts/{id}": {
      "get": {
        "summary": "Get a post’s status",
        "description": "Returns status, failedReason, publishedAt, and permalink (scope: post:read).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Post",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/media": {
      "post": {
        "summary": "Upload media",
        "description": "Uploads media and returns a mediaId (scope: media:write).",
        "responses": {
          "200": {
            "description": "Uploaded"
          }
        }
      }
    },
    "/api/v1/media/from-url": {
      "post": {
        "summary": "Add media from a public link",
        "description": "sprkly downloads the URL server-side and returns a mediaId (scope: media:write). Required for TikTok and YouTube video, which cannot publish from an external URL.",
        "responses": {
          "201": {
            "description": "Stored"
          },
          "422": {
            "description": "The URL could not be used (not a file, too large, unsupported type)"
          }
        }
      }
    },
    "/api/v1/media/upload": {
      "post": {
        "summary": "Request a media upload URL",
        "responses": {
          "200": {
            "description": "Upload target"
          }
        }
      }
    },
    "/api/v1/publish": {
      "post": {
        "summary": "Create a post for immediate publishing",
        "responses": {
          "200": {
            "description": "Queued"
          }
        }
      }
    }
  }
}