概要
API の概要
Sofia API v1 の全エンドポイントとそのパラメータ、スコープ、例。ご自身のキーで各エンドポイントを試せるプレイグラウンド付き。
ベース URL https://api.sofia-post.com/api · OpenAPI ドキュメント
Sofia API を使うと、コードから Sofia のアカウントにアクセスできます。Workspace の店舗、その接続済みネットワーク、メディアライブラリ、投稿、統計です。HTTPS の REST API で、リクエストとレスポンスは JSON です。次のことができます。
- 公開する:今すぐ、日時を指定して、または下書きとして、1 つまたは複数のネットワークに同時に公開します。
- 公開前に確認する:ドライランで、何も書き込まずに、各ネットワークが投稿を受け付けるかどうかが分かります。
- メディアを管理する:ファイルをアップロードするか、URL から店舗のメディアライブラリにインポートします。
- 投稿を追跡する:ネットワークごとの状態、パーマリンク、失敗の理由を取得できます。署名つき Webhook で通知を受けることもできます。
- 統計を取得する:接続済みアカウントごとに、Sofia が収集した最新の数値を取得します。
- ネットワークを接続する:ホスト型のリンクを作成し、オーナーが足りないネットワークを接続できるようにします。
SDK と連携
- `sofia` CLI:npm で
sofia-postとして公開されている CLI で、ターミナルから Sofia を操作します。 - MCP サーバー:Sofia を AI エージェントにつなぎます。
- Zapier:Sofia の連携は、個人キーで接続します。トリガーは「Post Published」と「Post Failed」、アクションは「Create Post」(公開、予約、下書き保存)、検索は「Find Post」です。Zapier の公開ディレクトリには、まだ申請していません。
認証
すべての呼び出しで、Authorization ヘッダーに Bearer トークンを付けます。Sofia は呼び出しのたびに、トークンに紐づく権限を再確認します。Workspace から外されたメンバーや、取り消された許可は、次のリクエストからトークンが無効になります。
認証の方法は 2 つあります。
| 方法 | 用途 | トークン |
|---|---|---|
| 個人キー | 自分のアカウントの自動化、スクリプト、エージェント | sofia_pk_live_…、有効期限まで有効 |
| OAuth、認可コード + PKCE | ユーザーが選んだ Workspace で、ユーザーのために動くプロダクト | sofia_live_at_…、1 時間、更新可能 |
| OAuth、クライアントクレデンシャル | 自分がオーナーの Workspace を自動化する、自社のサーバー | sofia_live_at_…、1 時間 |
API キー
自分の Workspace を自動化するなら、個人キーで十分です。Sofia アプリで設定 › API を開き、個人アクセスタブでキーを作成します。名前、有効期限、権限を選びます。キーは一度しか表示されません。コピーして、コードの中ではなくシークレットの保管庫に保存してください。
設定 › API › 個人アクセスでキーを作成します。キーは、あなたのすべての Workspace で、あなたの代わりに動作します。指定した店舗によって Workspace が決まり、その中でのあなたのロールが、キーにできることを決めます。
キーは、Bearer トークンとして Authorization ヘッダーに入れて送ります。
curl https://api.sofia-post.com/api/v1/establishments \
-H "Authorization: Bearer sofia_pk_live_…"`GET /v1/me` で、キーが誰のために動作するか、その Workspace と権限が分かります。
必要な権限(スコープ)がない呼び出しには、403 insufficient_scope が返ります。
OAuth2 トークン
利用者の Workspace で、利用者のために動作する SaaS プロダクトでは、OAuth クライアントを登録します。ユーザーが自分で選んだ Workspace で、あなたのアプリを許可します。OAuth のアクセストークンは sofia_live_at_… で始まり、有効期間は 1 時間で、使い方は同じです。
curl https://api.sofia-post.com/api/v1/establishments \
-H "Authorization: Bearer sofia_live_at_…"ほかの Sofia アカウントが接続するアプリを作りますか?PKCE つきの認可コードフロー、クライアントクレデンシャル、スコープはOAuth2で説明しています。
ベース URL
すべてのエンドポイントは、1 つのベース URL の下にあります。
https://api.sofia-post.com/api/v1レート制限
Sofia API は、15 分、24 時間、30 日の 3 つの期間でリクエストを数えます。
| トークン | 15 分 | 24 時間 | 30 日 |
|---|---|---|---|
| 個人キー(アカウントごと) | 150 | 1,000 | 10,000 |
| OAuth クライアント | 100 | 500 | 15,000 |
| OAuth クライアント、許可したユーザーごと(最大 1,000) | +10 | +25 | +100 |
ヘッダー
すべてのレスポンスで、IETF 形式で残りが分かります。
RateLimit: "15m";r=144;t=43, "24h";r=994;t=13543, "30d";r=9994;t=13543
RateLimit-Policy: "15m";q=150;w=900, "24h";q=1000;w=86400, "30d";q=10000;w=2592000r は残りのリクエスト数、t は期間がリセットされるまでの秒数、q はクォータ、w は期間の秒数です。
期間の上限に達した時
レスポンスは 429 rate_limit_exceeded で、Retry-After ヘッダーと、上限に達した期間が details に含まれます。その時間だけ待ってから再試行してください。
API を効率よく使う
- 投稿を繰り返し確認する代わりに、Webhook を登録します。
limitを最大 100 まで使い、一覧を少ないページ数で読み込みます。- めったに変わらないものはキャッシュします:プラットフォーム、店舗、接続済みアカウント。
エラー
すべてのエラーは、HTTP ステータスと同じ JSON エンベロープで返ります。code はコードが読む契約で、message は人間向けに書かれており、変わることがあります。
{
"error": {
"code": "invalid_request",
"message": "The request is invalid",
"details": [
{ "field": "targets[0].text", "code": "required", "message": "targets[0].text is required" }
],
"docUrl": "https://www.sofia-post.com/developers/errors#invalid_request",
"requestId": "2aab5943-6333-4caf-8478-25a85ebd4abf"
}
}detailsは詳細を示します:拒否されたフィールド、ネットワークの理由、上限に達した期間。docUrlは、このページにあるコードの説明へのリンクです。requestIdはリクエストを識別します。サポートに伝えてください。
エラーコード
| ステータス | コード | 意味 |
|---|---|---|
| 400 | invalid_request | ボディ、クエリ、またはパラメータが無効です。拒否されたフィールドごとに 1 件の詳細(field、code(required、is_string、max など)、message)が付きます。 |
| 400 | invalid_json | ボディが有効な JSON ではありません。 |
| 400 | invalid_cursor | このカーソルは Sofia API が返したものではありません。 |
| 400 | webhook_url_invalid | webhook のアドレスが https ではないか、プライベートアドレスを指しています。 |
| 400 | webhook_event_invalid | 不明な webhook イベントです。 |
| 400 | application_name_invalid | 設定 › API: クライアント名がない、または長すぎます。 |
| 400 | application_profile_invalid | 設定 › API: クライアントのプロフィールに無効な項目があります。詳細にその項目名が示されます。 |
| 400 | logo_invalid | 設定 › API: ロゴが拒否されました(180×180 px 以上、PNG、JPEG または WebP、2 MB 以下)。 |
| 401 | invalid_token | トークンがない、不明、期限切れ、または取り消されています。あるいは、トークンの持ち主が、トークンに付与された権限を失っています。WWW-Authenticate ヘッダーを参照してください。 |
| 401 | unauthorized | 認証が必要です。 |
| 402 | subscription_required | live の書き込み(ドライランを含む)には、有効なサブスクリプションまたはトライアルが必要です。 |
| 403 | insufficient_scope | トークンにエンドポイントのスコープがありません。詳細に該当のスコープが示されます。 |
| 403 | insufficient_role | この Workspace でのあなたのロールでは、この操作は許可されていません。 |
| 403 | live_mode_required | テストトークンでの実際の書き込みです。 |
| 403 | route_closed_to_applications | このエンドポイントはアプリケーションには公開されていません。 |
| 403 | forbidden | アクセスが拒否されました。 |
| 403 | session_required | 設定 › API: この操作にはトークンではなく Sofia のセッションが必要です。 |
| 403 | impersonation_forbidden | 設定 › API: この操作は借用したセッションでは拒否されます。 |
| 403 | workspace_owner_required | この操作は Workspace のオーナーだけが行えます。 |
| 403 | workspace_member_required | あなたはこの Workspace のメンバーではありません。 |
| 404 | route_not_found | このメソッドとパスに応答するエンドポイントはありません。 |
| 404 | establishment_not_found | 不明な店舗、またはトークンの範囲外です。 |
| 404 | media_not_found | 不明なメディア、またはトークンの範囲外です。 |
| 404 | post_not_found | 不明な投稿、またはトークンの範囲外です。 |
| 404 | webhook_not_found | 不明な webhook、または別のアプリケーションのものです。 |
| 404 | not_found | リソースが存在しません。 |
| 404 | application_not_found | 設定 › API: 不明なクライアントです。 |
| 404 | grant_not_found | 不明な認可、またはすでに取り消されています。 |
| 404 | personal_key_not_found | 設定 › API: 不明な個人キーです。 |
| 404 | consent_not_found | 認可リクエストの期限が切れたか、存在しません。認可エンドポイントからやり直してください。 |
| 405 | method_not_allowed | このエンドポイントでは、このメソッドは許可されていません。 |
| 409 | post_in_flight | ネットワークが投稿を送信中です。少し待ってから削除を再試行してください。 |
| 409 | post_not_editable | ネットワークがすでに投稿を送信したため、編集できません。 |
| 409 | account_already_connected | このネットワークはすでに店舗に連携されています。 |
| 409 | webhook_limit_reached | アプリケーションの webhook がすでに 10 件に達しています。 |
| 409 | idempotency_in_progress | この Idempotency-Key での最初のリクエストがまだ実行中です。 |
| 409 | conflict | リクエストがリソースの現在の状態と競合しています。 |
| 409 | scope_removal_forbidden | 設定 › API: クライアントの権限は追加できますが、削除はできません。 |
| 409 | public_client_has_no_secret | 設定 › API: パブリッククライアントにはシークレットがありません。 |
| 413 | payload_too_large | ファイルがアップロードの上限を超えています。 |
| 415 | unsupported_media_type | サポートされていないコンテンツタイプです。 |
| 422 | post_refused | ネットワーク、店舗、またはご利用のプランが投稿を拒否します。理由ごとに 1 件の詳細があり、それぞれに message が付きます。 |
| 422 | media_import_refused | この URL からメディアをインポートできませんでした。理由は詳細に記載されます。 |
| 422 | media_upload_refused | アップロードされたファイルは、サポートされている画像または動画ではありません。 |
| 422 | platform_not_offered | このネットワークは連携の対象外です。 |
| 422 | idempotency_key_reused | この Idempotency-Key は別のボディで使用されました。 |
| 422 | unprocessable_entity | リクエストは理解されましたが、処理できません。 |
| 429 | rate_limit_exceeded | レート制限のウィンドウが上限に達しました。Retry-After の時間だけ待ってください。 |
投稿が拒否される理由
post_refused は、details[].code に各理由を一覧します。
| コード | 意味 |
|---|---|
unsupported_platform | このネットワークはサポートされていません。 |
duplicate_platform | 同じネットワークが 2 回指定されています。 |
unsupported_format | このネットワークはこのフォーマットに対応していません。 |
caption_too_long | テキストが長すぎます(max、actual)。 |
too_many_hashtags | ハッシュタグが多すぎます(max、actual)。 |
too_many_mentions | メンションが多すぎます(max、actual)。 |
too_many_thread_posts | スレッドが長すぎます(max、actual)。 |
too_many_media | メディアが多すぎます(max、actual)。 |
media_mix_refused | このネットワークでは、写真と動画を混在させることはできません。 |
media_required | このネットワークには写真または動画が必要です。 |
media_type_refused | このネットワークは、この種類のメディアを受け付けません。 |
scheduled_in_past | scheduledAt が過去の日時です。 |
scheduled_at_invalid | 予約投稿には、未来の scheduledAt が必要です。 |
account_not_connected | この店舗には、このネットワークに連携されたアカウントがありません。 |
account_ambiguous | このネットワークには複数のアカウントが連携されています。accountId で 1 つ指定してください。 |
media_not_found | このメディアは、この店舗のライブラリにありません。 |
media_url_invalid | メディアの URL は、公開された https のアドレスである必要があります。 |
platform_unavailable | このネットワークは、まだ Sofia では利用できません。 |
publication_quota_exceeded | ご利用のプランの投稿数の上限に達しました。 |
エラーを処理する
- ロジックは
codeに基づき、messageには決して基づかせないでください。 429はRetry-Afterの後に、5xxは指数バックオフで再試行します。4xxを同じ内容で再試行してはいけません。まずリクエストを直してください。- 失敗するたびに
requestIdをログに記録します。
ページネーション
一覧(GET /v1/posts、GET /v1/media)は、新しい順に 1 ページずつ返ります。limit でページサイズを 1〜100(既定は 20)で指定します。
{
"data": [ … ],
"nextCursor": "WyIyMDI2LTEwLTI3VDE1OjExOjU5LjAxN1oiLCIx…"
}nextCursor を cursor として渡すと、次のページを読めます。nextCursor がなければ、最後に達しています。
let cursor;
do {
const url = new URL('https://api.sofia-post.com/api/v1/posts');
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const page = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${key}` } }).then((r) => r.json());
handle(page.data);
cursor = page.nextCursor;
} while (cursor);カーソルは不透明な値です。作成したり変更したりしないでください。Sofia が返していないカーソルには 400 invalid_cursor が返ります。
冪等性
ネットワークエラーのせいで、投稿が作成されたか分からなくなることがあります。POST /v1/posts と POST /v1/media に Idempotency-Key ヘッダーを付け、同じキーで再試行してください。Sofia は最初と同じレスポンスを返し、重複して作成することはありません。
curl -X POST https://api.sofia-post.com/api/v1/posts \
-H "Authorization: Bearer $SOFIA_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d @post.json- 新しい投稿ごとに新しいランダムなキー(UUID)を使い、その投稿の再試行では同じキーを使います。
- 同じキーで異なるボディを送ると、
422 idempotency_key_reusedが返ります。 - 最初のリクエストが処理中の間は、同じキーに
409 idempotency_in_progressが返ります。少し待って再試行してください。
テストモードとドライラン
ドライラン。POST /v1/posts に ?dry_run=true を付けると、Sofia は実際の投稿と同じチェック(各ネットワークのルール、接続済みアカウント、メディア、日付、プラン)を行い、何も保存も送信もしません。レスポンスの詳細は公開するページにあります。
テストモード。 テストモード(トークンは sofia_test_at_…)で作成したアプリは、本物の Workspace を読み取れますが、書き込みは一切しません。投稿はシミュレートされ、同じルールで判定され、実際の書き込みには 403 live_mode_required が返ります。設定 › API で作成したキーとクライアントはライブモードです。公開せずに確認するには、ドライランを使ってください。
OAuth
/v1/oauth/authorize認可を開始するユーザーのブラウザーで開きます。未知のクライアントや未登録の redirect_uri は 400、それ以外は Sofia の同意画面へリダイレクトします。GET/.well-known/oauth-authorization-server/api/v1/oauthサーバーメタデータを取得するSofia API の OAuth 2.0 認可サーバーメタデータ(RFC 8414)を返し、クライアントがエンドポイントと対応オプションを検出できます。POST/v1/oauth/tokenアクセストークンを発行するclient_credentials、authorization_code、refresh_token を 3600 秒有効なトークンに交換します。アカウント
プラットフォーム
店舗
/v1/establishments店舗を一覧表示するトークンが操作する Workspace の店舗を返します。この ID を、投稿、メディア一覧、インサイトの establishmentId に使います。GET/v1/establishments/{id}/accounts連携済みアカウントを一覧表示する店舗に連携されたネットワークを返します。トークンやシークレットは含まず、アカウントの名前と accountId の選択に必要な情報のみです。POST/v1/establishments/{id}/connection-links連携リンクを作成する本人が Sofia でネットワークを認可するホスト型リンクを返し、ネットワークのトークンはアプリに渡りません。有効期間 30 分、ライブアプリのみ。GET/v1/establishments/{id}/insights店舗のインサイトを取得するSofia が連携アカウントごとに収集した最新の有効な数値を返します。未収集のアカウントは 0 ではなく awaiting と表示されます。メディア
/v1/mediaメディアを一覧表示する店舗のメディアライブラリを新しい順にページ単位で返し、現在の有効期間内の署名付き URL を付けます。テストでもライブでも使えます。POST/v1/mediaメディアを取り込むファイル(multipart)か https URL を店舗のメディアライブラリに追加します。ライブアプリと有効な購読が必要です。GET/v1/media/{id}メディアを取得するライブラリのメディア 1 件を、現在の有効期間内の署名付き URL とともに返します。その ID を投稿で再利用できます。DELETE/v1/media/{id}メディアを削除するアプリと同じく、店舗のライブラリからメディアを外します。ファイル自体は消えず、予約済みの投稿は画像を保持します。投稿
/v1/posts投稿を一覧表示するトークンが届く店舗の投稿を新しい順に返します。店舗、ステータス、期間、作成者で絞り込め、ページ単位で取得できます。POST/v1/posts投稿を公開するライブアプリは即時公開・予約・下書き保存、テストアプリはシミュレーションのみ。?dry_run=true ならすべて検証し、何も書き込みません。GET/v1/posts/{id}投稿を取得する各ネットワークの状態、公開後のリンク、失敗時のエラーを含む投稿を返します。ライブは Workspace の任意の投稿、テストはシミュレーション分を読み取ります。PATCH/v1/posts/{id}投稿を編集する送信前なら targets の置換、scheduledAt の設定、draft と scheduled の切り替えができます。DELETE/v1/posts/{id}投稿を削除するアプリと同じく投稿を削除します。予約分は送信されず、公開済みの投稿はネットワークから削除されます。テストアプリには実際に削除するものがありません。Webhook
/v1/webhookswebhook を一覧表示するアプリケーションの webhook を、署名シークレットなしで返します。シークレットは登録時にしか返されません。POST/v1/webhookswebhook を登録するSofia が署名付き JSON で呼び出す https URL を登録します。最大 5 回再試行。シークレットは 1 回のみ表示、最大 10 件。DELETE/v1/webhooks/{id}webhook を削除するwebhook を削除します。再試行待ちの配信を含め、以後は何も受信しません。この呼び出しはボディを返しません。POST/v1/webhooks/{id}/testテストイベントを送信する署名付きの webhook.test イベントを通常どおり配信します。受信側を待たずに署名を確認できます。1 分あたり最大 5 回です。