social-media-api-best-practices について
ソーシャルメディアへの投稿連携を開発する人向けのリファレンススキルです。掲載された各 API の認証、レート制限、メディアアップロード、エラー処理を扱います。
主な機能
- 01プラットフォーム別のOAuthとトークン処理
- 02レート制限とリトライの方法
- 03メディアのサイズと形式の要件
- 04分割・再開可能な動画アップロード
- 05プラットフォーム別のエラーと仕様差
活用シーン
- 01ソーシャル API の認証実装
- 02レート制限に沿った投稿キュー設計
- 03大容量動画の安定したアップロード
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zernio-dev/ social-media-api-best-practices --skill social-media-api-best-practicesインストール後、Claude Code で/social-media-api-best-practicesを入力して読み込まれたか確認します。
同梱ファイルREADME.md
詳細
ソーシャルメディア API のベストプラクティス
13 のプラットフォームで 100 万件以上の投稿をスケジュールした実績に基づくパターンです。
1. 認証パターン
OAuth 2.0 プラットフォーム
Instagram(Meta Graph API)
- 2 段階のトークン交換:短期間(1 時間)→ 長期間(60 日)
auth_type: 'rerequest'を使用して、権限の再要求を強制します- トークン交換エンドポイント:
ig_exchange_tokenの grant type - スコープ:
instagram_business_basic、instagram_business_content_publish、instagram_business_manage_messages、instagram_business_manage_comments、instagram_business_manage_insights
Twitter/X(PKCE 必須)
- S256 コードチャレンジを使用した PKCE が必須です
- state にコード検証子を埋め込みます:
${state}-cv_${codeVerifier} - スコープ:
tweet.read、tweet.write、users.read、offline.access、media.write、dm.read、dm.write - アクセストークン:2 時間、リフレッシュトークン:長期間有効
function generateCodeVerifier(): string {
return crypto.randomBytes(32).toString('base64url');
}
function generateCodeChallenge(verifier: string): string {
return crypto.createHash('sha256').update(verifier).digest('base64url');
}
function extractCodeVerifierFromState(state: string): string {
const match = state.match(/-cv_(.+)$/);
return match ? match[1] : '';
}
TikTok
- API 監査の承認には、UX の完全な準拠が必要です
- 表示必須項目:
privacy_levelセレクター、コメント/デュエット/スティッチの切り替え - 商用コンテンツの開示が必須です
- スコープ:
user.info.basic、user.info.profile、user.info.stats、video.publish、video.upload、video.list
- 常にヘッダーに
X-RestLi-Protocol-Version: 2.0.0を含めます - API バージョンヘッダー:
LinkedIn-Version: 202511(一部のエンドポイントでは202505または202401を使用します) - アクセストークン:60 日、リフレッシュトークン:365 日
- 個人(
urn:li:person:)と組織(urn:li:organization:)の両方の投稿に対応します - 個人用スコープ:
openid、profile、r_basicprofile、email、w_member_social、w_member_social_feed、r_member_postAnalytics、r_member_profileAnalytics、r_1st_connections_size - 組織用スコープ:
w_organization_social、w_organization_social_feed、r_organization_admin、r_organization_social、r_organization_social_feed、r_organization_followers
YouTube
- リフレッシュトークンを取得するには、
access_type=offlineとprompt=consentの両方が必要です - ユーザーチャンネルとブランドアカウントの両方に対応します
- スコープ:
youtube.upload、youtube、youtube.force-ssl、yt-analytics.readonly
- ページアクセストークンはユーザートークンとは別です
/me/accountsを介してユーザートークンをページトークンに交換しますscheduled_publish_timeによる予約投稿に対応します- スコープ:
pages_manage_posts、pages_show_list、pages_read_engagement、pages_manage_engagement、pages_read_user_content、business_management、pages_messaging
Threads
- Instagram(Meta)と同様に、2 段階のトークン交換を行います
th_exchange_tokenの grant type を使用します- スコープ:
threads_basic、threads_content_publish、threads_manage_replies、threads_read_replies、threads_manage_insights、threads_delete
- リフレッシュトークンを使用する OAuth 2.0
- スコープ:
pins:read、pins:write、boards:read、boards:write、user_accounts:read
Google Business Profile
- スコープ
business.manage、userinfo.profile、userinfo.emailを使用する Google OAuth - My Business v4 API を使用します
- 長期間有効なトークンには
duration=permanentを指定した OAuth を使用します - 厳格なユーザーエージェント要件 - Reddit は説明的なユーザーエージェントを必須としています
- スコープ:
identity、submit、read、mysubreddits、privatemessages、history、edit、vote
const headers = {
'User-Agent': 'YourApp/1.0 by /u/YourUsername'
};
非 OAuth プラットフォーム
Bluesky(AT Protocol)
- 従来型の OAuth は使用せず、DID(分散識別子)を使用します
- AT Protocol の
accessJwtとrefreshJwtを使用して認証します - PDS(パーソナルデータサーバー)の解決が必要です
ExpiredTokenエラー時に自動でリフレッシュします
// Bluesky authentication
const session = await agent.login({
identifier: 'user.bsky.social',
password: 'app-password'
});
// session contains: accessJwt, refreshJwt, did, handle
Snapchat
- Basic 認証、許可リスト限定(Snapchat 開発チームの承認が必要です)
- 公開 API は限定的で、プロフィール API が中心です
- スコープ:
snapchat-profile-api
Telegram
- ボットトークンのみ - OAuth フローはありません
- トークン形式:
BOT_ID:SECRET - ユーザー認証はなく、ボットをチャット/チャンネルに追加する必要があります
2. レート制限の戦略
プラットフォーム固有の制限
| プラットフォーム | 主な制限 | 対策 |
|---|---|---|
| アカウントあたり1日100件の投稿 | キューに入れて分散 | |
| TikTok | 24時間あたり保留中のアップロードは5件 | 処理完了を待つ |
| 3段階の制限(アプリ+ユーザー+エンドポイント) | 3つすべてを確認 | |
| 一般利用は寛容、大量処理には厳格 | 慎重にバッチ処理 | |
| YouTube | チャンネルごとの1日あたりのアップロード割り当て | 割り当てを監視 |
| 1分あたり60リクエスト | ヘッダーに従う | |
| Threads | 厳しいレート制限 | すぐに失敗する |
Twitterの3段階のレート制限
Twitterには3段階の制限があります。すべて確認してください。
- アプリレベルの24時間制限(最も厳しい)
- ユーザーレベルの24時間制限
- エンドポイント固有のレート制限
function parseRateLimitHeaders(headers: Headers) {
return {
// Endpoint limits
remaining: parseInt(headers.get('x-rate-limit-remaining') || '0'),
reset: parseInt(headers.get('x-rate-limit-reset') || '0'),
// App-level 24h limits
appRemaining: parseInt(headers.get('x-app-limit-24hour-remaining') || '0'),
appReset: parseInt(headers.get('x-app-limit-24hour-reset') || '0'),
// User-level 24h limits
userRemaining: parseInt(headers.get('x-user-limit-24hour-remaining') || '0'),
userReset: parseInt(headers.get('x-user-limit-24hour-reset') || '0'),
};
}
指数バックオフのパターン
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 5000
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (!isRetryableError(error) || attempt === maxRetries) throw error;
const delay = Math.min(baseDelay * Math.pow(2, attempt), 30000);
await sleep(delay);
}
}
throw new Error('Max retries exceeded');
}
function isRetryableError(error: any): boolean {
const status = error.status || error.statusCode;
return [429, 500, 502, 503].includes(status);
}
3. メディア要件
プラットフォームごとの全制限
| プラットフォーム | 画像の最大サイズ | 動画の最大サイズ | アスペクト比 | 特記事項 |
|---|---|---|---|---|
| 8MB | ストーリーズは100MB、リールは300MB | フィードは4:5~1.91:1、ストーリーズは9:16 | カルーセルは10件 | |
| TikTok | 20MB | 4GB、3秒~10分 | 9:16固定 | 写真カルーセルは35件 |
| 5MB | 512MB、2分20秒 | 柔軟 | 画像は1~4枚 | |
| 8MB | 5GB | 柔軟 | 画像カルーセルは20枚 | |
| YouTube | サムネイルは2MB | 256GB | 16:9を推奨 | 再開可能なアップロード |
| 10MB | 4GB | 柔軟 | 複数画像は10枚 | |
| Threads | 8MB | 1GB、5分 | 縦向きの9:16 | カルーセルは10件 |
| 32MB | 2GB | 柔軟 | カバー画像が必要 | |
| Bluesky | 1MB | 50MB、3分 | 柔軟 | AT Protocol |
| Snapchat | 20MB | 500MB | 9:16 | AES 暗号化 |
| Google Business | 5MB | 該当なし | 柔軟 | 画像のみ |
| 20MB | 該当なし | 柔軟 | URL 経由 | |
| Telegram | 10MB | 50MB | 柔軟 | 4096文字制限 |
大きなファイルはストリーミングするのが鉄則
ファイル全体をメモリに読み込まないでください。
// BAD - loads entire file into memory
const buffer = await fetch(url).then(r => r.arrayBuffer());
// GOOD - streams directly
const response = await fetch(url);
await uploadToPlatform(response.body); // Pass the stream
問題のあるメディアソース
以下のホストは、メディアへの直接アクセスの代わりにHTMLを返したり、タイムアウトしたりします。
const PROBLEMATIC_HOSTS = [
'drive.google.com',
'docs.google.com',
'dropbox.com',
'onedrive.live.com',
'1drv.ms'
];
// Solution: Re-host to your own storage first
if (PROBLEMATIC_HOSTS.some(h => url.includes(h))) {
url = await reHostToStorage(url);
}
Dropbox の URL 修正
function fixDropboxUrl(url: string): string {
if (url.includes('dropbox.com') && url.includes('dl=0')) {
return url.replace('dl=0', 'dl=1');
}
return url;
}
4. 動画アップロードのパターン
分割アップロード(Twitter)
async function chunkedUpload(videoBuffer: Buffer, mediaType: string) {
const CHUNK_SIZE = 4 * 1024 * 1024; // 4MB chunks
// INIT
const initRes = await api.post('/media/upload', {
command: 'INIT',
total_bytes: videoBuffer.length,
media_type: mediaType
});
const mediaId = initRes.media_id_string;
// APPEND chunks
for (let i = 0; i < Math.ceil(videoBuffer.length / CHUNK_SIZE); i++) {
const chunk = videoBuffer.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
await api.post('/media/upload', {
command: 'APPEND',
media_id: mediaId,
media: chunk.toString('base64'),
segment_index: i
});
}
// FINALIZE
await api.post('/media/upload', {
command: 'FINALIZE',
media_id: mediaId
});
// Poll for processing
await pollProcessingStatus(mediaId);
return mediaId;
}
再開可能なアップロード(YouTube)
YouTube は、大きなファイルを確実にアップロードできるよう、再開可能なアップロードをサポートしています。youtube-chunked-upload モジュールを使うか、再開可能なアップロードプロトコルを実装してください。
処理状況のポーリング
async function pollProcessingStatus(mediaId: string, maxWaitMs = 300000) {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const status = await api.get(`/media/upload?command=STATUS&media_id=${mediaId}`);
if (status.processing_info?.state === 'succeeded') return;
if (status.processing_info?.state === 'failed') {
throw new Error(status.processing_info.error?.message || 'Processing failed');
}
const checkAfter = (status.processing_info?.check_after_secs || 5) * 1000;
await sleep(checkAfter);
}
throw new Error('Processing timeout');
}
5. エラー処理
エラーの分類
type ErrorType = 'refresh-token' | 'retry' | 'user-error';
function categorizeError(platform: string, error: any): ErrorType {
if (isTokenError(error)) return 'refresh-token';
if (isTemporaryError(error)) return 'retry';
return 'user-error';
}
Instagram のエラーコード
| コード | 意味 | 対処 |
|---|---|---|
| 2207001 | スパムを検出 | ユーザーエラー |
| 2207003 | メディアのダウンロードがタイムアウト | 再試行 |
| 2207004 | 画像が大きすぎる(>8MB) | 圧縮 |
| 2207006 | メディアが見つからない | ユーザーエラー |
| 2207026 | サポートされていない動画形式 | 再エンコード |
| 2207042 | 1日あたりの投稿数が100件を超過 | 24時間待つ |
| 2207050 | ユーザーが制限されている | ユーザーエラー |
| 2207051 | ブロックされた(ただし投稿済みの可能性あり!) | 確認 |
| 2207052 | メディアの取得に失敗 | 直接 URL を使用 |
Instagram のエッジケース:2207051
Instagram のスパム対策により、「ブロックされた」と返されても、実際には投稿されている場合があります。
async function handleInstagram2207051(accountId: string) {
await sleep(5000);
const recentMedia = await getRecentMedia(accountId);
const justPosted = recentMedia.find(m =>
Date.now() - new Date(m.timestamp).getTime() < 60000
);
if (justPosted) {
return { success: true, postId: justPosted.id };
}
throw new Error('Post actually failed');
}
TikTok のエラーコード
| コード | 意味 | 対処 |
|---|---|---|
| access_token_invalid | トークンの有効期限切れ | 更新 |
| scope_not_authorized | 権限がない | 再接続 |
| rate_limit_exceeded | リクエストが多すぎる | 再試行 |
| spam_risk_* | コンテンツがフラグ付けされた | ユーザーエラー |
| file_format_check_failed | 無効な形式 | ユーザーエラー |
| unaudited_client_can_only_post_to_private | 開発モード | 非公開のみ |
Twitter/X のエラーコード
| コード | 意味 | 対処 |
|---|---|---|
| invalid_grant | トークンが取り消された | 再接続 |
| usage-capped | レート制限中 | 再試行 |
| duplicate | 同じコンテンツが存在 | ユーザーエラー |
| 186 | ツイートが長すぎる | ユーザーエラー |
Bluesky のエラーコード
| コード | 意味 | 対処 |
|---|---|---|
| ExpiredToken | JWT の有効期限切れ | 自動更新 |
| InvalidToken | 無効なトークン | 再接続 |
| XRPCNotSupported | アプリパスワードにDM権限がない | 完全な認証を使用 |
Reddit エラーコード
| コード | 意味 | 対処 |
|---|---|---|
| invalid token | 有効期限切れ | 更新 |
| not allowed to submit | サブレディットの制限 | ユーザー側のエラー |
| RATELIMIT | リクエストが速すぎます | 間隔を空けて再試行 |
| NO_LINKS | リンクは許可されていません | ユーザー側のエラー |
Telegram エラーコード
| コード | 意味 | 対処 |
|---|---|---|
| message too long | 4096 文字超 | 切り詰め |
| chat not found | 無効なチャット ID | ユーザー側のエラー |
| bot was blocked | ユーザーがボットをブロックしました | ユーザー側のエラー |
6. プラットフォーム固有の注意点
JavaScript の大きな整数 ID(Instagram/Facebook)
Instagram の ID は MAX_SAFE_INTEGER(17 桁以上)を超えます:
function safeJsonParse(text: string) {
// Wrap large integers in quotes before parsing
const safe = text.replace(/"id"\s*:\s*(\d{15,})/g, '"id":"$1"');
return JSON.parse(safe);
}
Twitter の文字数カウント
function countTwitterCharacters(text: string): number {
let count = 0;
// URLs always count as 23 characters
const urlRegex = /https?:\/\/[^\s]+/g;
const urls = text.match(urlRegex) || [];
const textWithoutUrls = text.replace(urlRegex, '');
count += urls.length * 23;
// Emojis and CJK count as 2
for (const char of textWithoutUrls) {
count += char.match(/[\u{1F600}-\u{1F64F}]|[\u4e00-\u9fff]/u) ? 2 : 1;
}
return count;
}
Twitter のプラン別文字数制限
| プラン | 制限 |
|---|---|
| 無料 | 280 |
| Premium | 4,000 |
| Premium+ | 25,000 |
LinkedIn のテキストエスケープ
予約文字:| { } [ ] ( ) < > # \ * _ ~
注:投稿に \@ が表示されるのを避けるため、@ は意図的にエスケープしていません。URN のメンション @[Name](urn:li:person:ID) とハッシュタグ #tag は自動的に保持されます。
function escapeLinkedInText(text: string): string {
// Reserved chars (excluding @ to preserve readability)
const reserved = /[\|\{\}\[\]\(\)\<\>\#\\\*\_\~]/;
let output = '';
let i = 0;
while (i < text.length) {
// Preserve URN mentions: @[Display Name](urn:li:person:ID)
if (text[i] === '@' && text[i + 1] === '[') {
const closeBracket = text.indexOf(']', i + 2);
if (closeBracket !== -1 && text.substring(closeBracket + 1, closeBracket + 9) === '(urn:li:') {
const closeParen = text.indexOf(')', closeBracket + 9);
if (closeParen !== -1) {
output += text.substring(i, closeParen + 1);
i = closeParen + 1;
continue;
}
}
}
// Preserve hashtags: #word
if (text[i] === '#' && /[a-zA-Z0-9_]/.test(text[i + 1] || '')) {
output += text[i++];
while (i < text.length && /[a-zA-Z0-9_]/.test(text[i])) {
output += text[i++];
}
continue;
}
// Escape reserved characters
output += reserved.test(text[i]) ? '\\' + text[i] : text[i];
i++;
}
return output;
}
TikTok の UX コンプライアンス要件
TikTok の API 監査では、UX 要件への完全な準拠が必要です:
- ユーザーがプライバシーレベルを手動で選択する必要があります(デフォルト値は不可)
- コメント/デュエット/スティッチの切り替え項目を表示する必要があります
- ユーザーの確認を伴う商用コンテンツの開示
- アップロード前のコンテンツプレビュー
- 明示的な同意の宣言
Reddit の User-Agent 要件
Reddit は説明的なユーザーエージェントを厳格に要求します:
// BAD - will be blocked
headers['User-Agent'] = 'axios/1.0';
// GOOD - descriptive format
headers['User-Agent'] = 'MyApp/1.0 (by /u/developer_username)';
Telegram の HTML サブセット
次の HTML タグのみがサポートされています:
<b>, <strong>
<i>, <em>
<u>
<s>, <strike>, <del>
<code>
<pre>
<a href="...">
<tg-spoiler>
<blockquote>
7. 特殊なプロトコル
AT Protocol(Bluesky)
Bluesky は OAuth ではなく、AT Protocol を使用します:
主な概念:
- DID(分散識別子):ユーザーの永続的な ID(例:
did:plc:abc123) - PDS(パーソナルデータサーバー):ユーザーデータが保存される場所
- ハンドル:人が読める名前(例:
user.bsky.social)
リッチテキストのファセット:
メンションとリンクでは、バイトオフセットによる位置指定を使用します:
function createFacets(text: string): Facet[] {
const facets: Facet[] = [];
const encoder = new TextEncoder();
// Find mentions
const mentionRegex = /@([a-zA-Z0-9.-]+)/g;
let match;
while ((match = mentionRegex.exec(text)) !== null) {
const beforeText = text.slice(0, match.index);
const byteStart = encoder.encode(beforeText).length;
const byteEnd = byteStart + encoder.encode(match[0]).length;
facets.push({
index: { byteStart, byteEnd },
features: [{
$type: 'app.bsky.richtext.facet#mention',
did: await resolveDid(match[1])
}]
});
}
return facets;
}
Snapchat の AES-256-CBC 暗号化
Snapchat では、アップロード前にメディアを暗号化する必要があります:
import crypto from 'crypto';
function encryptForSnapchat(buffer: Buffer): {
encrypted: Buffer;
key: string;
iv: string;
} {
const key = crypto.randomBytes(32); // 256 bits
const iv = crypto.randomBytes(16); // 128 bits
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
const encrypted = Buffer.concat([
cipher.update(buffer),
cipher.final()
]);
return {
encrypted,
key: key.toString('base64'),
iv: iv.toString('base64')
};
}
8. 簡易デバッグチェックリスト
投稿に失敗した場合:
- トークンの有効性を確認 - アクセストークンの有効期限が切れていませんか?
- レート制限を確認 - 日次/時間単位の制限に達していませんか?
- メディア URL を確認 - プラットフォームから直接取得できますか?
- メディア仕様を確認 - 形式、サイズ、寸法、アスペクト比は適切ですか?
- 2207051 のケースを確認 - エラーが出ても投稿されていませんか?
- アカウントの状態を確認 - アカウントが制限されていませんか?
- サブレディットのルールを確認 - (Reddit)リンクは許可されていますか?フレアは必要ですか?
- ユーザーエージェントを確認 - (Reddit)十分に説明的ですか?
- ボットの権限を確認 - (Telegram)送信権限のあるチャットにボットが参加していますか?
9. 推奨アーキテクチャ
your-app/
├── lib/
│ ├── platforms/
│ │ ├── base.ts # Abstract base class
│ │ ├── instagram.ts
│ │ ├── tiktok.ts
│ │ ├── twitter.ts
│ │ ├── bluesky.ts # AT Protocol
│ │ ├── snapchat.ts # With encryption
│ │ └── ...
│ ├── utils/
│ │ ├── rate-limiter.ts
│ │ ├── media-handler.ts
│ │ ├── error-mapper.ts
│ │ └── encryption.ts # For Snapchat
│ └── queue/
│ └── scheduler.ts
10. プラットフォーム比較一覧
| 機能 | TikTok | YouTube | Threads | Bluesky | Snapchat | GBP | Telegram | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| OAuth | 2 段階 | 標準 | PKCE | 標準 | 標準 | 2 段階 | 基本 | AT Proto | 基本 | 標準 | ボットトークン | ||
| トークンの有効期間 | 60 日 | 2 年以上 | 2 時間以上+更新 | 更新は 365 日 | 1 年 | 60 日以上 | 60 日 | 該当なし | 自動 | 該当なし | 1 年 | 無期限 | 該当なし |
| 動画 | あり | あり | あり | あり | あり | あり | あり | あり | あり | あり | なし | なし | あり |
| カルーセル | 10 | 35 | 4 | 20 | なし | 10 | 10 | なし | 4 | なし | なし | ギャラリー | アルバム 10 |
| スケジュール設定 | API | 下書き | なし | API | API | API | API | なし | API | なし | なし | なし | なし |
| DM | あり | なし | あり | あり | なし | あり | なし | なし | あり | あり | なし | あり | あり |
| アナリティクス | あり | あり | あり | あり | あり | あり | あり | あり | なし | なし | 制限あり | なし | 制限あり |
このガイドについて
このガイドは、Lateが管理しており、13のプラットフォームすべてで100万件以上の投稿をスケジュールする中で得た実際のパターンをもとに作成されています。
ここに記載されているすべてのエラーコード、例外的なケース、特有の挙動は、実運用での経験に基づいています。Instagramの2207051の落とし穴は、何時間もデバッグした末に発見しました。Snapchatの暗号化要件は、苦労して学びました。
ソーシャルメディアアプリを開発していますか? これらの連携をすべて自分で実装・保守するのは避けたいという方には、LateのAPIが複雑な処理を担います。OAuth、メディア処理、レート制限、エラー処理、スケジュール設定を、1つの連携で13のプラットフォームすべてに対応できます。
Lateが管理 — 開発者向けソーシャルメディアスケジュール設定API