note非公式APIを徹底調査|2026年版エンドポイント一覧完全版

下記冒頭の書き出しは記事公開当初の名残です。
本記事の現在の最終更新は 2026-08-05 です。
2026 年 5 月時点で観察した範囲では、note 非公式 API のエンドポイント一覧解説として最も網羅的なはずの記事 です (たぶん)。マガジン CRUD、メンバーシップ ( /api/v2/circle/... )、掲示板 ( /api/v2/boards/... )、有料記事編集時の note_draft 取り扱い、認証 API の reCAPTCHA v3 必須化 (2026 年 5 月下旬の破壊的変更) まで、自分の実機検証ベースで洗い出してあります。
初版公開は 2026 年 5 月ですが、note 側の仕様変更を追いかけて随時更新しており、直近では 2026 年 8 月に GraphQL API まわりの発見を追記しています。更新の経緯は記事末尾の更新履歴にまとめてあります。
出発点は ego_station さん・fuji1080 さん・nori_nw さんなどの先行記事で、それらを 2026 年時点の挙動に合わせて検証・更新し、過去 2 年で増えた範囲 (マガジン CRUD、メンバーシップ、掲示板まわり等) も含めて拡張しています。「いや、もっと網羅的なやつあるよ」というのを見つけた方は、ぜひ教えてください。

はじめに
スコープ
note.com の非公式 API のうち、自分が観察した範囲のエンドポイント一覧
リクエスト method・path・主要 payload フィールド
既存解説記事との差分 (新規発見、訂正)
想定読者
note の挙動を理解したいエンジニア
公式 API がない中で自分用の便利スクリプトを書きたい人
非公式 API の実態を知っておきたい note ユーザー
注意書き
note の非公式 API は 公式に提供されているものではない ため、規約や robots.txt 上はグレーゾーンです。仕様変更も頻繁にあり、いつ動かなくなってもおかしくない前提で扱う必要があります。
本記事はあくまで「動作観察の記録」であり、運営の迷惑になるような大量アクセスや業務利用を勧めるものではありません。低頻度の個人利用にとどめてください。
不可視 reCAPTCHA や挙動変更で塞がれる可能性も常にある、という前提を共有した上で読み進めてください (実際、ログインは 2026 年 5 月下旬から reCAPTCHA v3 で塞がれました。詳しくは「認証 API 一覧」の追記を参照)。
観察方法
エンドポイントの調査は、ブラウザの DevTools の network panel と、ページに JavaScript を注入して fetch / XHR を hook する手法を併用しました。
Chrome DevTools の network panel
最も基本の手段です。フィルタを note.com/api/ あたりに絞ると、ページ遷移や操作に応じて飛ぶリクエストが一通り見えます。
ただし note の Web UI は Next.js の SSR や Service Worker、サーバーアクションを挟んでいる箇所があり、network panel に出てこないリクエスト がいくつかありました。特に記事の publish ボタンクリック時の PUT /api/v1/text_notes/{id} は、自分の環境では DevTools のリクエスト一覧に出てこなかったケースがあります。
fetch / XHR の monkey-patch

network panel で取りこぼすケースに対しては、ページに次のような JS を注入して全リクエストを記録する方法が有効でした。
const orig = window.fetch.bind(window);
window.fetch = async function(input, init) {
const url = typeof input === "string" ? input : input.url;
const method = (init && init.method) || "GET";
const body = init && init.body ? String(init.body).slice(0, 2000) : null;
console.log({ url, method, body });
return orig(input, init);
};
const oxopen = XMLHttpRequest.prototype.open;
const oxsend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this.__url = url; this.__method = method;
return oxopen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(body) {
console.log({ url: this.__url, method: this.__method, body: body && String(body).slice(0, 2000) });
return oxsend.apply(this, arguments);
};
これを Tampermonkey の userscript に仕立てて note.com と editor.note.com に常駐させると、リロードや SPA 遷移を跨いでも全リクエストを localStorage に記録できます。 POST/PUT/DELETE の payload もそのまま見えるので非常に便利です。
書込 API 観察用の拡張版 (ヘッダーまで捕獲)
上記の基本版は URL / method / body は拾えますが、リクエストヘッダーは取れていません。これだと「特定のカスタムヘッダーが付いていないと書込系が 500 を返す」というクラスの要件を見落とします (実際、後述する v3 コメント API の X-Note-Client-Code ヘッダ要件はヘッダーを観察するまで気付けませんでした)。
書込系 API を本気で調査するときは、以下のような拡張版を用意して ** POST / PUT / PATCH / DELETE に絞ってヘッダーごと localStorage に積む** hook を仕込んでおくと、見落としを大幅に減らせます。
const STORE = "__note_api_write_tap";
const MAX_ENTRIES = 100;
const URL_MATCH = /\/api\/v\d+\//;
const WRITE_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
function headersToObj(h) {
if (!h) return {};
if (h instanceof Headers) {
const o = {}; h.forEach((v, k) => { o[k] = v; }); return o;
}
if (Array.isArray(h)) return Object.fromEntries(h);
return { ...h };
}
function bodyToStr(b) {
if (b == null) return "";
if (typeof b === "string") return b;
if (b instanceof FormData) {
const parts = [];
b.forEach((v, k) => parts.push([k, typeof v === "string" ? v : `[${v.constructor?.name}]`]));
return "[FormData] " + JSON.stringify(parts);
}
if (b instanceof Blob) return `[Blob ${b.size}]`;
if (b instanceof URLSearchParams) return b.toString();
try { return JSON.stringify(b); } catch { return String(b); }
}
function record(method, url, headers, body) {
const m = String(method || "GET").toUpperCase();
if (!WRITE_METHODS.has(m) || !URL_MATCH.test(url)) return;
const list = JSON.parse(localStorage.getItem(STORE) || "[]");
list.push({ ts: new Date().toISOString(), method: m, url, headers, body: bodyToStr(body) });
while (list.length > MAX_ENTRIES) list.shift();
localStorage.setItem(STORE, JSON.stringify(list));
}
const origFetch = window.fetch.bind(window);
window.fetch = function (input, init) {
const url = typeof input === "string" ? input : (input && input.url) || "";
const method = (init && init.method) || (input && input.method) || "GET";
const headers = { ...headersToObj(input && input.headers), ...headersToObj(init && init.headers) };
record(method, url, headers, init && init.body);
return origFetch(input, init);
};
const oOpen = XMLHttpRequest.prototype.open;
const oSetHeader = XMLHttpRequest.prototype.setRequestHeader;
const oSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this.__m = method; this.__u = url; this.__h = {};
return oOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.setRequestHeader = function (k, v) {
(this.__h = this.__h || {})[k] = v;
return oSetHeader.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
record(this.__m, this.__u, this.__h || {}, body);
return oSend.apply(this, arguments);
};
DevTools のコンソールで JSON.parse(localStorage.getItem('__note_api_write_tap') || '[]') と打てば、これまでに記録された書込リクエストを一覧で見られます。 X-CSRF-Token / X-Note-Client-Code / Authorization / Origin / Referer などのヘッダーがそのまま見えるので、daemon や CLI スクリプトで再現するときに「どのヘッダーを必須として送るか」を取りこぼしません。
書込系に絞っていてかつ URL も /api/v\d+/ でフィルタしているので、 /api/v3/trackings/fp のようなテレメトリ系も自動的に除外されて、ノイズが激減します。ページのフォーム操作・記事公開・コメント投稿などをひと通り実機で叩いてから、localStorage の内容をエクスポートして daemon 実装の参考にする、という運用が便利です。
観察のコツ
「投稿する」「更新する」のような 取り消しのきかないボタン をクリックする直前に必ず hook を仕込む。クリック後はリダイレクトで monkey-patch が消えるので、localStorage に書く形にして永続化しておくと取りこぼしが減る
編集中の draft も ?draft=true クエリを付けた GET で取得できるので、編集画面を観察するときは URL のクエリパラメータも見落とさない
リクエストの種類が多い導線では、不要な tracking 系 ( /api/v3/trackings/fp など) をフィルタで除外しておくと見やすい
認証 API 一覧
note への認証はメール + パスワードのセッション Cookie 方式です。
POST /api/v1/sessions/sign_in
リクエストボディに { "login": "メールアドレス", "password": "..." } を JSON で送ります。成功すると _note_session_v5 Cookie がセットされ、以降の API は同じ Cookie 付きで叩けば認証済みとして扱われます。
note には現状 2FA が無く、CAPTCHA も通常のログインでは出てきません。プログラム的にログインできてしまうのは便利な反面、注意すべき点でもあります。 (2026-05-22 追記により打ち消し: 下記の追記参照)
reCAPTCHA トークン無しで叩く → HTTP 2xx で返ってくるが Set-Cookie: _note_session_v5=... ヘッダが付かない。エラーレスポンスでもないので、クライアント側からは「成功したのにセッションが取れない」謎現象に見える
reCAPTCHA トークン有り (= ブラウザ経由の通常ログイン) → 従来通り Cookie が発行されて以後の API が叩ける
【2026-05-22 追記】このエンドポイントに reCAPTCHA v3 が必須化されました
2026 年 5 月下旬 (私の観測ベースでは 2026-05-21〜) から、 POST /api/v1/sessions/sign_in のリクエスト payload に ** g_recaptcha_response フィールド (reCAPTCHA v3 のトークン) が必須** になりました。エンドポイント自体は同じで、メソッドも path も変わっていません。変わったのは payload に reCAPTCHA トークンが入っていない場合の挙動で、以下のように "成功っぽいけど Cookie だけ発行されない" 挙動になります。
旧 payload は {"login": "...", "password": "...", "redirect_path": ""} の 3 フィールドでしたが、現在はこれに g_recaptcha_response: "0cAFcWeA..." が加わっています。Chrome の DevTools Network panel で Copy as cURL するとそのままトークン入りの curl が取れるので、現状を観察したい人はそれが早いです。
結果として、自前スクリプトから email + password だけでログインしてセッションを取る経路は事実上塞がれました。reCAPTCHA v3 のトークンは Google 側の SDK + 正規 site key + ブラウザ環境がないと生成できないため、サーバサイドや CLI から偽造して付与することは現実的ではありません。本記事で紹介していた "メール + パスワードで叩いて _note_session_v5 を回収する" やり方は、2026 年 5 月以降の新規セッションでは使えません。
回避策として現実的なのは、普段使いの Chrome 等にログイン済の _note_session_v5 Cookie を OS の鍵保管庫経由で復号 → 自前ツール側に注入する 方法です。Chrome の cookie store (SQLite) と OS の鍵保管庫 (Linux なら libsecret、macOS なら Keychain、Windows なら DPAPI) を組み合わせれば自前プロセスから読めるので、Python なら browser_cookie3 のようなライブラリで一発、それ以外の言語でも同等の実装が存在します。実機の note 自動化は CAPTCHA 越えを目指すより、この方向に振った方が現実的でした。
暗号化アーキテクチャのキーワードだけ:
encrypted_value[:3] で v10 / v11 / v20 を分岐 / Linux & macOS は PBKDF2 派生鍵 + AES-CBC / Windows は DPAPI-wrapped key + AES-GCM / v20 (Chrome 127+ Windows) は app-bound encryption で原則 user-mode 不可 (これを無効化する feature flag が Chrome 側に存在する可能性はあるが未調査) / 詳細は Chromium の components/os_crypt/ 参照。
GET /api/v2/current_user
ログイン中ユーザーの基本情報を返します。 /api/v2/current_user/email という亜種もあり、こちらは email を含む詳細情報が返ります。
Cookie の永続化
_note_session_v5 Cookie は数か月程度の有効期間があるようで、定期的なログイン操作なしでも問題なく使い続けられました。Cookie を永続化しておけば、毎回サインインを叩く必要はありません。
記事まわりの API 一覧(GET 系)
note の API は内部的に「記事」「マガジン」「メンバーシップ」「ユーザー」「掲示板」「ハッシュタグ」「認証」のリソース系統に分かれていて、それぞれ別の path 配下にぶら下がっています。

note の記事まわりは大部分が v3 の API に集約されていますが、一部 v2 / v1 が混在しています。
GET /api/v3/notes
note タイムライン (公開記事の流し見) を取得します。クエリパラメータ kind / status などはそのまま pass-through で渡せます。
GET /api/v3/notes/{key}
note 記事の詳細を取得します。 {key} は n で始まる文字列 (例: n85fcb635c0a9 )。レスポンスには name , body , description , price , separator , is_my_note , is_purchased , can_* フラグなどが入っています。
** ?draft=true&draft_reedit=false クエリを付ける** と、編集者視点での詳細が返ります。これが地味に重要で、有料記事の場合は data.note_draft.body に 無料部分と有料部分が合成された全 HTML が入ります。同時に data.note_draft.separator に境界となる block id (UUID) が入っていて、この UUID を持つ段落までが無料、それ以降が有料、という構造になっています。
GET /api/v3/searches
横断検索です。クエリパラメータは context=note|user|magazine|hashtag|circle|noteForSale , q=キーワード , size=10 , start=0 のような形で、context を切り替えることで note / ユーザー / マガジン / ハッシュタグ / メンバーシップ / 有料 note を横断的に検索できます。
sort パラメータで並び順を切り替えられます (UI の「人気」「急上昇」「新着」タブに対応)。
sort=popular : 人気順 (デフォルト)
sort=hot : 急上昇 (直近の伸び率)
sort=new : 新着順 (publish 日時降順)
size は 1 ページの件数 (実機では 20 が上限の様子)、 start はページング offset。 popular ソートは 1,000 件くらいまでページングしてもエラーにならないことを確認していますが、それ以上は未検証です。
GET /api/v2/creators/{urlname}
クリエイターページの情報を取得します。 {urlname} は URL の note.com/<urlname> 部分。
GET /api/v2/creators/{urlname}/contents
クリエイターの記事一覧。クエリ kind=note&page=1 を付けます。自分自身の場合は info というリテラルが urlname の位置に使えます ( /api/v2/creators/info/contents?kind=note&page=1 )。
⚠️ この一覧には「クリエイターページに非表示」設定 ( exclude_from_creator_top: true ) の記事が含まれません。
実機観察 (2026-08、記事数の多いあるアカウント): このアカウントのプロフィールが返す noteCount は 99 件なのに、この一覧を isLastPage: true まで巡回すると 56 件で打ち切られました。
最初はページネーションのバグを疑いましたが、消えた 43 件は全件この設定になっているだけで、 GET /api/v3/notes/{key} で個別に読めば status: published のまま普通に実在します。バグではなく仕様です。
全件を欠落なく取得したい場合は、後述の GraphQL 経由の月別アーカイブを使うのが確実です。
GET /api/v2/creators/{urlname}/archives
年ごとの投稿件数サマリーを返します。 exclude_from_creator_top やピン留めの影響を受けず、
月別アーカイブ ( note.com/{urlname}/archives ) のトップページが使っているのと同じデータです。
{
"data": [
{
"year": "2026",
"totalNum": 94,
"details": [
{"summaryDate": "2026-08", "month": "08", "num": 1},
{"summaryDate": "2026-07", "month": "07", "num": 15}
]
},
{
"year": "2025",
"totalNum": 5,
"details": [{"summaryDate": "2025-12", "month": "12", "num": 5}]
}
]
}
details[].num の合計は GET /api/v2/creators/{urlname} ( GET /api/v2/current_user 等) の
noteCount と一致することを確認済みです。「投稿があるかどうか」の年月一覧だけを返す軽量な
エンドポイントで、各年月の中身 (どの記事が何件あったか) までは返しません。中身の取得には
後述の GraphQL 版アーカイブクエリを使います。
このエンドポイント単体では地味ですが、まず REST のこのエンドポイントで投稿がある年月の
一覧を取り、それぞれの年月だけ GraphQL 版アーカイブクエリで問い合わせれば、全件を過不足なく
集められます。 記事全件取得の要になるエンドポイントです。
GET /api/v2/note_list/contents
自分の note 一覧 (web UI のダッシュボードが使っているやつ)。 page=1 などのクエリ付き。
note.com の GraphQL API ( graphql.note.com ) ※2026-08 追記
note.com の Web フロントエンドは、記事一覧まわりの取得を REST から GraphQL に移行しています。エンドポイントは POST https://graphql.note.com/graphql 一本で、 operationName / query / variables を含む Apollo Client 標準の payload を送るだけです。
代表的なクエリを 2 つ確認しています。
CreatorAllNotesPageQuery ( creatorNotesConnectionByUrlname(urlname, first, after, isPinnedOnCreatorHomeExcluded: true) ): クリエイターページの新着一覧と同じデータ。 first / after のカーソルベース (Relay-style) ページネーション。REST の /contents と同じく、 exclude_from_creator_top: true の記事は含まれません。
CreatorArchivesPageQuery ( noteArchivesConnectionByUrlname(urlname, year, month, first, after) ): 月別アーカイブページ ( note.com/{urlname}/archives/{year}/{month} ) が使うクエリ。こちらはクリエイターページに非表示設定の記事も含めて、その年月の全記事を返します。 全件を欠落なく取りたいときは、このクエリを対象年月ぶん呼ぶのが正攻法です (どの年月に投稿があるかは、後述の REST 版サマリー API で先に確認できます)。
どちらも NoteListItem というフラグメントを共有していて、実用上は common.publishedAt / common.likeCount / common.commentCount / openContents.title / pricing.isFree / pricing.onetimePurchaseLowestPrice あたりを拾えば十分です。GraphQL 特有の __typename や Base64 エンコードされたグローバル id は無視して問題ありません。
__type introspection クエリで NoteListItem の全フィールド ( common / private / creator / openContents / pricing 等) を確認したところ、「クリエイターページに非表示」( exclude_from_creator_top ) に相当するフィールドはどこにも存在しませんでした。 ( common ( NoteCommonData ) にはピン留め関連の isPinnedOnCreatorHome / pinnedCreatorNoteId はあるものの、非表示設定は無し。 private ( NotePrivateData ) も isHavingDraft のみ。) つまり CreatorArchivesPageQuery が非表示設定の記事を返すのは、「フラグを無視している」のではなく、このスキーマ自体が exclude_from_creator_top という概念を持っていない から、というのが正確な理解です。この設定は REST v3 の記事詳細 ( GET /api/v3/notes/{key} ) だけが持つフィールドのようです。
CreatorArchivesPageQuery は「その年月に何が投稿されたか」は返しますが、「そもそも何年何月に投稿があるか」までは教えてくれません。どの年月に投稿があるかは、前述の REST 版サマリー API ( GET /api/v2/creators/{urlname}/archives ) で先に取得できます。この2つを組み合わせた全件取得の手順は、そちらの見出しを参照してください。
REST と GraphQL の併用は、記事一覧まわりに限った話ではなさそうです。 note.com/contests (お題・コンテスト一覧) では専用 API 自体が存在せず、Next.js の RSC (React Server Components) ストリーミングでページが描画されていて、REST でも GraphQL でもない第三の配信経路になっていました。記事一覧側は GraphQL への移行がすでに一段落しているように見える一方、ページによって実装がバラついている状態なので、この移行の範囲は今後も広がっていく可能性がある と見ておいた方が良さそうです。少なくとも記事一覧・月別アーカイブでは GraphQL 側が正なので、 /api/v2/creators/{urlname}/contents のような REST 系エンドポイントは将来的に非推奨・縮退していく前提で付き合うのが安全だと思います。
GET /api/v2/hashtags/{tag}
ハッシュタグの情報を取得します。v1 ( GET /api/v1/hashtags/{tag} ) も並列で生きていて、レスポンス形が違うので必要に応じて切り替えます。
v2 のレスポンスには id / name / count のほか、** relatedHashtags フィールド (関連タグの {name, count} の配列)** が含まれます。関連タグの取得は v2 でのみ可能で、v1 にはこのフィールドがありません。タグナビゲーション系の UI を作るときに重宝します。
GET /api/v3/hashtags/{tag}/notes
特定ハッシュタグに紐づく記事の一覧を取得します。クエリパラメータ:
order=popular|new|hot : 並び順 (人気 / 新着 / 急上昇)。他の値を入れると 400 invalid order type で弾かれます
page=N : ページング (1 ページ 50 件)
paid_only=true|false : 有料記事のみに絞るかどうか
レスポンスは { notes[], count, next_page, is_last_page } 形式で、各 note は /api/v3/notes と同じスキーマ ( key / name / body / eyecatch_url / user / like_count / publish_at など) を持ちます。 GET /api/v2/hashtags/{tag} の relatedHashtags と組み合わせると、「あるタグ → 関連タグ → 記事一覧」の導線を API だけで完結できます。
GET /api/v2/categories
カテゴリ一覧。 data.categories に注目・マンガ・コラム・エッセイなどの top カテゴリが並びます。各カテゴリの key 値 (例: music , it , gadget ) は有料マガジン作成時の categories 指定でも使われます。
GET /api/v1/categories/{name}
カテゴリ別の記事一覧。 name は上記の key 値。クエリ note_intro_only=true&sort=new&page=1 などを取ります。
GET /api/v1/magazines/{key}
マガジン情報を取得します。 {key} は m で始まる文字列。
GET /api/v3/notice_counts
通知件数 (未読 DM 数、未読通知数、未読インフォメーション数) を取得します。
GET /api/v1/stats/pv
自分の記事の PV 統計を取得します。クエリ filter=all&page=1&sort=pv などを取ります。
記事の作成・公開・編集・削除 API 一覧
note の記事 (text_note) は、内部的にステータスを持つステートマシンになっています。
ステータス遷移の全体像
新規作成のときは status: "" (= 未保存に近い予約状態) で始まり、内容を保存すると "draft" (下書き)、公開すると "published" に上がります。published からは soft delete で "deleted" まで進められます。
もう一つ、予約投稿 ( publish_at を未来の日時にして PUT する) 専用の status: "reserved" が存在します (2026-08-05 実機観察)。素直に status="published" + 未来の publish_at を送ると 422「不正なパラメータが渡されました」で拒否されます。note の Web エディタは予約投稿時に status を "reserved" に切り替えて送っていて、これを真似ると 200 で通ります。予約日時が来ると note 側が自動で "published" に遷移させます。
POST /api/v1/text_notes
空の draft 予約を作成します。空 body の POST で OK。レスポンスに id (numeric) と key ( n... 形式) が返り、 status: "" の状態になります。この時点では UI 上は表示されません。
POST /api/v1/text_notes/draft_save?id={numeric_id}
下書き保存です。 { "name": "...", "body": "<HTML>" } を JSON で送ります。これを叩くと status: "draft" に昇格し、自分のダッシュボードに表示されるようになります。差分上書き方式なので、本文を変更するたびに body 全体を投げます。
** hashtags / magazine_ids / circle_permissions などの publish 系メタフィールドはこのエンドポイントでは silently 無視されます** (200 / result: true で返ってきますが、永続化はされません)。これらの設定は publish ( PUT /api/v1/text_notes/{id} ) 経由でないと反映できないので、下書き段階で段階的に組み立てる用途には使えません。
PUT /api/v1/text_notes/{numeric_id}
publish (draft → published) です。フル payload (status="published" を含む全フィールド) を送ります。主要フィールドは下記。

status : 必須。 "published"
name : タイトル
free_body : 無料部分 HTML。各段落は <p name="UUID" id="UUID">...</p> のように block id を持つ
pay_body : 有料部分 HTML (有料記事のみ)
separator : free_body の末尾段落の id (= 無料部分の境界)
price : 価格 (0 で無料、100〜50000)
slug , body_length , hashtags , image_keys , magazine_ids , magazine_keys
disable_comment , limited , is_refund , index
exclude_from_creator_top , exclude_ai_learning_reward
send_notifications_flag , author_ids
circle_permissions : メンバーシップ限定公開の指定 (詳細は後述の小節)。空配列で「限定しない」
discount_campaigns , lead_form , line_add_friend , line_add_friend_access_token , pro_coupon_keys
このエンドポイントは publish 専用ではなく、公開済み記事の編集にも同じものを使います。「更新する」ボタンを押したときも同じ PUT が status="published" で叩かれます。差分更新の API は存在せず、毎回フルセットを送る必要があります。
publish payload で 空 body の 500 を返す 4 つの罠
実装で PUT /api/v1/text_notes/{id} を叩くと、レスポンス body が完全に空のまま 500 だけ返ってくる挙動に何度かハマりました。Tampermonkey で Web 編集画面が実際に送っている payload をキャプチャして突き合わせたところ、note 側で deserialize や検証に失敗すると エラーメッセージなしの 500 で落ちる原因は下記 4 つでした。
** image_keys を空配列にしない**: free_body / pay_body 中の <img src=".../img/<key>.<ext>"> の <key> を出現順に列挙して詰めます。Web エディタは画像 upload のたびにこの配列を更新していて、空のまま送ると note 側の整合性検証で落ちます
** lead_form / line_add_friend に null を入れない**: null だと note の deserializer が落ちます。未使用でも {"is_active": false, "consent_url": ""} / {"is_active": false, "keyword": "", "add_friend_url": ""} のような 空オブジェクト を入れます
** body_length は HTML 長ではなく可視テキスト長**: free_body.length + pay_body.length だと HTML タグ込みのバイト数になりますが、note は 編集画面の文字数カウンタと同じ値 (タグ除去後の Unicode コードポイント数) を期待しています。これがズレると 500
** slug は空文字を送らない**: 未指定時のデフォルトは Web エディタが使う slug-<note_key> パターンが安全。空文字を送ると URL の挙動が壊れます。なお slug フィールド自体は API レイヤーで受け付けて DB に保存されるものの、URL ルーティングには一切効きません: note_url / OG / canonical / 共有 URL はすべて key ( n... ) ベースに固定されており、 https://note.com/<user>/n/<custom-slug> のような URL は HTTP 404 になります。実質的には旧仕様の vestigial field です
note 側でエラー詳細を返してくれないので、Web の payload を実機キャプチャして差分を埋める作業がいちばん早い解決経路でした。
追記 (2026-08-05): 上記の slug トラップは、通常の status="published" よりも status="reserved" (予約投稿) のほうがシビアに効くことを確認しました。 slug も note_key も指定せず publish しようとした場合、status="published" なら 422 の構造化エラー ( {"error":{"code":"invalid","message":"..."}}) が返ってきたのに対し、status="reserved" だと本節冒頭の「空 body の 500」がそのまま再現しました。予約投稿を試すときほど slug (または note_key 経由の自動解決) を必ず渡すべきです。
circle_permissions の shape (メンバーシップ限定公開)
circle_permissions の配列要素は {kind, keys} 形式で、 kind の値で 2 モードを切り替えます。
メンバー全員に公開: {"kind": "circle", "keys": ["<circle_key>"]}
特定プラン限定公開: {"kind": "circle_plan", "keys": ["<plan_key>", ...]}
複数プランを指定する場合は 同じ要素の keys 配列に並べる 形 ( {"kind":"circle_plan","keys":["plan_a","plan_b"]} ) で、要素を増やすのではありません。 circle_key (単数) や circle_plan_key (単数) という名前のフィールドは存在せず、 keys (複数形) で必ず配列を渡す点に注意。
circle key とプラン key は GET /api/v3/memberships/circle_permissions のレスポンスから取得できます ( data[].circle.key / data[].circle_plans[].key )。
circle_permissions: [] (空配列) で再 publish すると、既存のメンバーシップ紐付けを外せます (後述の unpublish 制約を回避したいときの常套手段)。
POST /api/v2/notes/{key}/change_status
公開済み記事を draft に戻す (unpublish) ためのエンドポイント。body は {"status":"draft"} 。published → draft の 一方向専用 で、draft → published を叩くと 403 で弾かれます。
下記カテゴリに該当する記事は HTTP 403 で拒否され、 status は変わりません (response body の data フィールドに拒否理由のメッセージが入ります):
(a) 有料記事
(b) 過去に有料記事として販売実績のある記事
(c) メンバーシップ特典記事 / 特典マガジンに追加されている記事
(d) 有料マガジン / 定期購読マガジンに追加したことがある記事
(c) は ** circle_permissions: [] で再 publish してメンバーシップ紐付けを外してから unpublish** すれば通ります。(a)(d) も該当フィールドを外せば理屈の上では通るはずですが、(b) は履歴ベースの判定なので不可逆と思われます (未検証)。
unpublish が通らないケースでも、直接 DELETE /api/v1/notes/{numeric_id} で soft delete することは可能で、こちらにはこれらの制約がありません (メンバーシップ紐付き記事でも問題なく status=deleted へ遷移)。
DELETE /api/v1/text_notes/draft_delete?id={numeric_id}
draft 状態の記事を削除します。published 状態のものには使えません。
DELETE /api/v1/notes/{numeric_id}
公開記事の削除 (soft delete)。author 視点では status: "deleted" として残ります が、一般ユーザーからは見えなくなります。
/api/v1/text_notes/... ではなく /api/v1/notes/... 配下なので注意。
編集用の取得: GET /api/v3/notes/{key}?draft=true
すでに触れましたが、編集画面で表示される「最新の差分」を取得するためのフォームです。Web UI の編集画面が叩いているのはこのエンドポイントで、 data.note_draft.body に full HTML が、 data.note_draft.separator に境界 UUID が入ります。
ただし、編集中差分 (note_draft) は publish 直後に消えることがあるので、再編集時には note_draft が null の状態に出くわすことがあります。
メディア系 API 一覧(画像 / 添付 / 埋め込み)
note の記事に挿入できるメディアは大きく 3 種類あり、それぞれ別のエンドポイントを使います。
画像: 2 段アップロード
POST /api/v3/images/upload/presigned_post
→ data.action と data.post.* (S3 presigned form fields) が返る
POST <data.action> (= S3 のドメイン) multipart で file + presigned fields
→ 204
最終的に <figure name=UUID id=UUID><img src="/https://assets.st-note.com/img/<KEY>" alt="..." width=W height=H></figure> の形で本文 HTML に埋め込みます。
追記 (2026-08-05): presigned_post が返す S3 の post fields に、STS 一時認証情報 ( ASIA... prefix) 用の x-amz-security-token フィールドが増えていることを実機確認しました。以前は key / acl / Expires / policy / x-amz-credential / x-amz-algorithm / x-amz-date / x-amz-signature の 8 fields で足りていましたが、いつからか x-amz-security-token が追加で返ってくるようになっていて、これを S3 への multipart POST に含めずに送ると 403 InvalidAccessKeyId で拒否されます。field 自体は返ってきているので、古い実装だと「レスポンスに含まれているのに使っていないフィールド」として見落としがちです。S3 へ POST するときは、presigned_post のレスポンスに含まれる post.* フィールドを 全部そのまま転記するのが安全です (将来また field が増減しても追従できる)。
添付ファイル: 1 段直接アップロード
POST /api/v2/attachments/upload
multipart で { file: <binary>, file_name, note_key }
→ { attachment_key, filename, size, embedded_content_key, type: "attachment" } が返る
S3 presigned ではなく note.com サーバーに直接 POST します。 note_key (記事の n... key) が body に必須で、つまり draft 予約後でないと添付できません。
見出し画像 (eyecatch / サムネイル)
POST /api/v1/image_upload/note_eyecatch
multipart で { note_id: <draft_id>, file: <binary>, width: <px>, height: <px> }
→ { data: { url: "https://assets.st-note.com/production/uploads/images/<id>/rectangle_large_type_2_<hash>.png" } }
本文用の画像と違って、こちらは 2 段アップロードではなく note サーバーへの 1 段 multipart です。 note_id (numeric draft id) を form に乗せると サーバー側で draft に直接紐付けされる ため、別途 draft_save で eyecatch フィールドを送る必要はありません。レスポンスの URL は 本文画像とは別フォーマット ( /production/uploads/images/<id>/rectangle_large_type_2_<hash>.png ) で、サーバ側で複数サイズ画像が生成されています。
二点ハマりやすい挙動:
** Content-Type (MIME type) を multipart の file part に明示しないと 500** が返ります。拡張子から image/png / image/jpeg / image/gif / image/webp を判別して Content-Type ヘッダに含める必要があります
** note_id は数値 id** ( /api/v1/text_notes の応答の id )。 n... 形式の key を入れると 400
URL を別フォーマットで持つせいで、 <img> から <key> を抜く際の正規表現が共通化しづらく、本文中の画像 ( <img src=".../img/<key>.<ext>"> ) と eyecatch ( <img src=".../production/uploads/.../<hash>.png"> alt="eyecatch") は別経路で扱う必要があります。
埋め込み (YouTube / X など)
GET /api/v2/embed_by_external_api/check_type?url=<URL>
→ { data: { type: "..." | null } }
GET /api/v2/embed_by_external_api?url=<URL>&service=<TYPE>&embeddable_key=<NOTE_KEY>&embeddable_type=Note
→ { data: { key: "emb...", html_for_embed: "..." } }
三点ハマりやすい挙動があります。
check_type は YouTube などの主要サービスに対して null を返す ことがあります。実は Web UI 側で URL パターンマッチで先に判定しているらしく、check_type は補助的な役割。 youtube.com / youtu.be / x.com / twitter.com / instagram.com / soundcloud.com / spotify.com / vimeo.com / tiktok.com / github.com/.../gist/... あたりは自分で URL パターンマッチを持っておくのが現実的
embed_by_external_api の embeddable_type は "Note" (TextNote ではない)、 embeddable_key は 編集中 note の n... key (numeric id ではない)。これを満たさないと 400「不正なリクエストです」で弾かれます
**note 内記事の URL を埋め込みたい場合は service=external-article ** を指定します。check_type は null を返しますが、service を external-article で明示すると embedded_content_key ( emb... ) と「外部記事カード」用の html_for_embed が返ってきます。ただしこれは OGP ベースの「外部記事カード」形式で、Web UI が実際に生成しているネイティブな note 埋め込みとは別物です。詳細は次項を参照してください
note 内記事のネイティブ埋め込み ( POST /api/v1/embed )
上の embed_by_external_api (v2) とは別に、
note 内記事どうしの埋め込みだけは v1 系の別エンドポイントが使われていることが
2026-07-11 の実機観察でわかりました。
note の記事一覧やタイムラインでよく見る、
サムネイル・タイトル・抜粋・スキ数・著者・投稿日が並んだ
コンパクトなカード( <iframe class="note-embed"> )は、
external-article の OGP カードとは見た目も仕組みも別物です。
Web UI で「+」→「埋め込み」を選んで note 内記事の URL を貼ったときに、
実際に飛んでいるリクエストはこちらでした。
POST /api/v1/embed (multipart/form-data)
url: <埋め込みたい note 記事の URL>
height: 211
embeddable_type: Note
embeddable_key: <埋め込み先の note の key (n... 形式)>
レスポンスは v2 系と envelope の形が違い、
embedded_content という 1 段ネストが挟まります。
{
"data": {
"embed_to": null,
"embedded_content": {
"key": "emb16218f1081f7",
"url": "https://note.com/<urlname>/n/<key>",
"service": "note",
"identifier": "<key>",
"embeddable_type": "Note",
"html_for_embed": "<iframe class=\"note-embed\" height=\"211\" ... src=\"https://note.com/embed/notes/<key>\"></iframe>"
}
}
}
data.embedded_content.key を embedded_content_key として使えば、
本文 HTML には
<figure data-src="<URL>" embedded-service="note" embedded-content-key="emb...">
という形で保存されます。
観察した範囲でのポイント:
** GET /api/v2/embed_by_external_api/check_type にこの URL を投げても意味がない**。
note 内記事の判定はフロントエンド側で URL パターン ( note.com/<urlname>/n/<key> ) を見て、
v1/embed に直接振り分けているようですマガジン URL ( note.com/<urlname>/m/<key> ) は対象外で、
従来通り external-article の OGP カード扱いになりますheight は観察した範囲では常に 211 固定でした。これがサーバー側で使われる値なのか、
クライアント側の表示用ヒントに過ぎないのかは未検証です
添付ファイル取得 API 一覧(※既存記事で触れられていなかった部分)
添付ファイルは前述のとおり POST /api/v2/attachments/upload で アップロード できますが、その対になる ダウンロード / 記事内の添付一覧抽出 にも非公式 API があり、こちらも既存記事ではほぼ触れられていません。
GET /api/v2/attachments/download/{hash}
添付ファイルの実体をダウンロードするエンドポイント。 {hash} は 32 文字 16 進数で、アップロード成功時のレスポンス attachment_key がそのまま使われます。Cookie ベースの認証で叩けば、 Content-Type と Content-Disposition がそのまま返ってくるため、 curl -OJ でファイル名つきで保存できます。
curl -OJ "https://note.com/api/v2/attachments/download/d980bdc8a6224c130dad35cc9df2abd8"
記事 body 内の添付ファイル一覧
note の編集画面で挿入した「ファイルダウンロード」ブロックは、最終的に記事 body の HTML 内に下記のような <figure> として現れます。
<figure name="UUID" id="UUID" embedded-service="attachment" embedded-content-key="emb...">
<a href="/https://note.com/api/v2/attachments/download/<32hex>" rel="...">
<strong>CreateArticles.zip</strong> 17.6 KB ファイルダウンロードについて
ダウンロード
</a>
</figure>
つまり「記事内のすべての添付ファイル一覧」を取りたい場合は、専用 API を叩く必要はなく、 GET /api/v3/notes/{key} の data.body を取得して <figure embedded-service="attachment"> を抽出すれば OK です。各 <figure> から <a href> の 32hex hash、 <strong> のファイル名、 </strong> 直後のサイズラベル ("17.6 KB" 等)、 id 属性 (block id) が取り出せます。
ダウンロード URL の hash 抽出は以下の正規表現一発で済みます。
/\/api\/v2\/attachments\/download\/([0-9a-fA-F]{32})(?:[/?#]|$)/
抽出した hash を HEAD /api/v2/attachments/download/{hash} に投げると、本体をダウンロードする前に Content-Type と Content-Length だけ拾えます (HEAD を拒否されたら GET にフォールバックして bytes 自体を捨てれば同等)。記事を巡回して添付ファイルの種類とサイズだけ把握したいケースに便利です。
マガジン API 一覧(※既存記事で触れられていなかった部分)
note のマガジンは「自分のマガジンを作成・更新・削除」する API と、「記事をマガジンに紐付ける」操作で構成されます。後者については、専用エンドポイント ( POST /api/v1/our/magazines/{magazine_key}/notes ) と publish payload 同梱 ( PUT /api/v1/text_notes/{id} の magazine_ids / magazine_keys ) の 2 ルートが現存することが今回の調査で判明しました (詳細は後述)。
GET /api/v1/my/magazines
自分のマガジン一覧。これは ego_station さんの一覧表にも載っていなかったエンドポイントです。
POST /api/v1/my/magazines
マガジンを作成します。payload は販売モードによって項目が変わります。
無料マガジンの payload:
{
"name": "...",
"description": "...",
"status": "public",
"price": 0,
"subscribe": false,
"categories": []
}
有料 (単体販売) マガジンの payload ( status="public" , price>0 , categories 必須):
{
"name": "...",
"description": "...",
"status": "public",
"price": 500,
"subscribe": false,
"categories": ["gadget"]
}
有料 (定期購読) マガジンの payload (note プレミアム契約が必要):
{
"name": "...",
"description": "...",
"status": "public",
"subscribe": true,
"price": 100,
"frequency": 1,
"is_free_subscribe": true,
"content": "...",
"target_number": "10",
"management_name": "...",
"categories": ["it", "business"]
}
定期購読は categories が 2 つ必須、 frequency (月次更新回数: 1 / 2 / 4 / 10 / 20 / 30) も必須、note プレミアムに加入していないと UI 上で作成ボタンが押せません。作成後は note 運営の審査プロセスが走ります。
PUT /api/v1/our/magazines/{key}
マガジン本体の更新。path が /our/ 配下である点に注意 (作成・削除の /my/ ではなく)。フィールドはフル payload を要求するので、name / description / price / status / is_immediate_charge を全部送る必要があります。
DELETE /api/v1/my/magazines/{key}
マガジン削除。これは即時削除で、UI からも実行できる操作です。
記事をマガジンに紐付ける: 2 ルートある
「記事をマガジンに登録する」操作には現在 2 つの API ルートがあります。
ルート 1: 専用エンドポイント (記事単位の紐付け)
POST /api/v1/our/magazines/{magazine_key}/notes
body: { "note_id": <numeric note id> }
→ 201 { data: { status: "success", note_status: "published", limited_note_included_magazine_keys: [...] } }
DELETE /api/v1/our/magazines/{magazine_key}/notes/{note_key}
→ 200 { data: { status: "success", ... } }
path は **マガジンの hex key ** ( m... で始まる) を取り、POST 側 body の note_id は 記事の numeric id、DELETE 側 path の {note_key} は 記事の hex key ( n... )、と微妙に非対称なので注意。試した範囲ではこの組み合わせ以外 (numeric magazine id を path にする、 note_key を POST body に入れる、など) は 404 / 400 で弾かれました。
ego_station さんの 2024 年版一覧表にも POST /api/v1/our/magazines/{id}/notes というエンドポイントが書かれていて、当初の調査時には「現在は 404 で動かない」と本記事で書いていたのですが、これは私が ** {id} をマガジンの numeric id と解釈して叩いていた** ためでした。hex key で叩けば現在も生きています。前記事への訂正としてここに残しておきます。
ルート 2: publish payload に同梱 (記事編集の一部として紐付け)
publish 時の PUT /api/v1/text_notes/{id} payload に magazine_ids: [<numeric>, ...] または magazine_keys: ["m...", ...] を含める方法です。 magazine_ids と magazine_keys は どちらか片方を送れば追加され、union 動作なので両方併用しても問題ありません。空配列を送ると所属解除されます。
"magazine_ids": [1818414],
"magazine_keys": ["md9e3a0d087f4"]
Web UI で記事編集中に「マガジンに追加」を選んだ際に飛ぶリクエストはルート 2 で、publish フローの一部として一緒に送られます。
どちらを使うか
既に公開済みの記事を後から特定マガジンに足したいだけ → ルート 1 (publish の差分情報を再構築せずに済むので楽)
記事の他フィールド (title / body / hashtags など) も同時に編集する → ルート 2 (どうせフル PUT になる)
一括処理で複数記事を 1 つのマガジンに紐付ける → ルート 1 (シンプル)
メンバーシップ API 一覧(※既存記事で触れられていなかった部分)
note のメンバーシップ機能は内部的に「circle」と呼ばれていて、 /api/v2/circle/... という path 配下にエンドポイントが集約されています。先行記事ではほとんど触れられていない領域です。
設計上の重要な制約
1 ユーザーあたり 1 つしかメンバーシップを持てない。 POST /api/v2/circle は初回のみ可能。path に key を含まず、Cookie 認証で対象を特定する設計
メンバーシップ本体の削除 API は存在しない。アカウントを消すまで「持っていない状態」には戻せない (名前・説明は更新可)。試し叩き厳禁
記事のメンバーシップ紐付けは、マガジンと違い publish payload ( PUT /api/v1/text_notes/{id} ) の circle_permissions フィールド経由のみ。専用エンドポイント ( POST /api/v1/our/circles/.../notes 的なもの) は今回探した限り存在せず。 circle_permissions の shape は publish 周りの章 (「circle_permissions の shape」小節) を参照
GET /api/v2/circle/memberships/summaries
自分が関わっている (加入中 + 運営中) メンバーシップを 1 つのリストで返します。各エントリの summary.circle.isOwner で運営側か判別できます。
GET /api/v2/circle?canva_id=true
自分のオーナーとしてのメンバーシップ情報。key、name、description、boardKey (掲示板の key) などを取得できます。
POST /api/v2/circle
メンバーシップを作成します (1 回のみ)。body は { "name": "...", "description": "...", "type": "circle" } のシンプルな形。
繰り返しになりますが、これは不可逆操作です。削除 API がないので、誤って叩いたら永続的に残ります。
PUT /api/v2/circle
メンバーシップ本体を更新。body は { name, description, tmp_header_image_key, is_delete_header_image } を全フィールド送ります。
GET /api/v2/circle/plans
自分のプラン一覧。
POST /api/v2/circle/plans
プランを作成します。body のフィールドは下記。
{
"name": "ベーシック",
"description": "...",
"membership_notes_enabled": true,
"membership_magazines_enabled": false,
"signup_enabled": false,
"thanks_message": "",
"benefit_appeal_texts": [],
"circle_plan_apply_urls": [],
"is_free_subscribe": false
}
作成直後は price 未設定 / 募集オフ の状態。価格は次の PATCH で別途設定する 2 段階構成です。
PATCH /api/v2/circle/plans/{plan_key}
プラン更新。マガジンと同じく フル payload を要求します。下記の全フィールドを毎回送る必要があります。
{
"name": "...",
"description": "...",
"benefit_appeal_texts": [],
"signup_enabled": false,
"thanks_message": "",
"withdrawal_message": "",
"membership_notes_enabled": true,
"membership_magazines_enabled": false,
"circle_plan_apply_urls": [],
"is_board_enabled": true,
"tmp_header_image_key": "",
"price": 300,
"subscribe_price_in_next_month": { "is_immediate_charge": true }
}
部分更新の API はないので、自前で叩く場合は事前に GET /api/v2/circle/plans でプラン情報一式を取得して、変更したいフィールドだけ差し替えて PATCH する形になります。
POST /api/v2/circle/plans/{plan_key}/suspend
プラン停止 (実質削除)。これは「即時削除」ではなく 月末まで継続 されます。既存メンバーへの請求・コンテンツ閲覧期間が絡むため、申請が完了して月末で停止、という運用です。
UI 上では削除確認のモーダルでプラン名を再入力する必要があります (タイポ予防)。
POST /api/v3/memberships/plans/{plan_key}/magazines
プランの「特典マガジン」を置き換えるエンドポイント。body は { "magazine_keys": [...] } 。空配列を送ると紐付けを解除できます。
GET /api/v2/circle/members
自分のメンバーシップに参加しているメンバーの一覧 (オーナー視点)。
GET /api/v3/memberships/{circle_key}/notes
メンバーシップ限定で公開している記事の一覧。
GET /api/v2/creators/{urlname}/circle
公開ページ視点でクリエイターのメンバーシップを取得します。
GET /api/v2/creators/{urlname}/circle/plans
公開ページ視点でのプラン一覧。
掲示板 API 一覧(board / メンバーシップに付属)
メンバーシップには「メンバー限定の掲示板」が標準で付随しており、これも独立した API になっています。 circle.boardKey で取れる key を使います。
GET /api/v2/boards/{board_key}/posts
掲示板の投稿一覧。クエリ order_key=sort_updated_at&order_param=desc&exclude_comments=true&per=3 などを取ります。
POST /api/v2/boards/{board_key}/posts
投稿作成。body は下記。
{
"title": "...",
"body": "...",
"read_permission": { "kind": "circle" }
}
read_permission.kind は "circle" (メンバー全員に公開) または "plan" ( plan_keys で絞り込み)。後者の payload は { "kind": "plan", "plan_keys": ["..."] } 。
DELETE /api/v2/boards/{board_key}/posts/{post_key}
投稿削除。レスポンスは 204 No Content。
GET /api/v2/boards/{board_key}/pinned_post
掲示板のピン留め投稿。ピン留めが無い場合は 404 が返ります。
ソーシャル API 一覧(スキ / コメント / フォロー)
POST/DELETE /api/v3/notes/{key}/likes
スキを付ける / 取り消す。
GET /api/v3/notes/{key}/likes
その記事にスキを付けたユーザーの一覧。
GET /api/v3/notes/{key}/note_comments
記事のコメント一覧。2026 春までは GET /api/v1/note/{numeric_id}/comments だったのですが、現在は v1 系が常に空配列を返す挙動になっていて、実 UI は v3 の note_comments に切り替わっています。
クエリパラメータは page / per_page / order ( newest デフォルト / oldest ) / parent_key (返信スレッド取得用、 nc... 形式の親コメント key) です。 n... 形式の記事 key を path に直接取るので、事前に numeric id を引く必要はなくなりました。
レスポンスは {current_page, next_page, total_count, data: [...]} の形で、 data に各コメントの配列が入ります。v1 とは違って data はオブジェクトではなく 配列 が直接入る点に注意 (v1 用のコードが data.comments を見ていたら破壊的変更になります)。各要素は以下のような shape です:
key — コメントの key。** nc... 形式** ( nc99c174b7175e 等、記事 key の n... とは別系統)
comment — 本文の AST (後述、root + p + text のネスト)
user — { key: 32hex, nickname, urlname, profile_image_url } 。**ユニーク識別子は user.key **、旧 v1 にあった numeric id は v3 では返ってきません
is_root — false ならスレッド返信
reply_count / latest_creator_reply / is_creator_replied / like_count / is_edited 等
POST /api/v3/notes/{key}/note_comments
コメント投稿。書込系 (POST/PUT/DELETE) には 3 つの非自明な要求 があります。
** X-Note-Client-Code ヘッダが必須**。値は 64 hex のクライアント識別子。note の Web UI は初回 SSR 時にサーバから配られた値を localStorage["note-client-code"] に保存して使い回しています。再現する場合は uuid v4 を 2 つ連結 (32 hex × 2) して送れば通りました。形式と長さ以外のチェックは無さそうです (実機観察ベース、公式仕様は未確認)。
** comment フィールドはプレーン文字列ではなく AST**。形は { "type": "root", "children": [ { "type": "element", "tag_name": "p", "children": [ { "type": "text", "value": "本文" } ] } ] } 。プレーン文字列を送ると 500 ("予期せぬエラー") で弾かれます。段落分割は children に <p> element を並べる形で表現します。
** acknowledgement: false が必須**。body の top-level に入れます。省略すると 500。
返信を投稿する場合は body に parent_key (親コメントの nc... 形式 key) を足します。
PUT /api/v3/notes/{key}/note_comments/{comment_key}
既存コメントの編集。body は { "comment": <AST> } 。 X-Note-Client-Code ヘッダが必須なのは POST と同じです。 acknowledgement と parent_key は不要でした。
DELETE /api/v3/notes/{key}/note_comments/{comment_key}
コメント削除。path 末尾は **数値 comment_id ではなく nc... 形式の comment_key ** (リスト取得レスポンス各要素の key フィールド、 nc99c174b7175e 等) を取ります。 X-Note-Client-Code ヘッダ必須。
POST/DELETE /api/v3/users/{user_id}/following
フォロー / フォロー解除。path は numeric user_id。urlname から id を引くには GET /api/v2/creators/{urlname} でレスポンスの data.id を見ます。
既存記事と異なる点 (訂正)
参考にした記事のうち、現在の note では動かないエンドポイントがいくつかありました。
GET /api/v2/hashtags (ハッシュタグ一覧)
ego_station さんの一覧表に載っていますが、現在は 404 です。ハッシュタグの一覧取得は別の path に統合されたか、廃止された可能性があります。タグ単独の情報取得 ( GET /api/v2/hashtags/{tag} ) は引き続き生きています。
GET /api/v1/followings/{userId}/list / GET /api/v1/followers/{userId}/list
ego_station さんの一覧表に載っているフォロー一覧 API ですが、現在は 404 が返ります。urlname を渡しても numeric id を渡しても結果は同じでした。
GET /api/v3/users/{userId}/followings を試すと 403 (forbidden) が返り、こちらは権限制御で塞がれているようでした (自分自身の id を渡しても 403)。正しいフォロー一覧 API の path は今回特定できていません。
POST /api/v1/our/magazines/{id}/notes (マガジンに記事を登録) — {id} の解釈に注意
ego_station さんの一覧表に書かれているこの「マガジンに記事を登録する API」を ** {id} をマガジンの numeric id と解釈して叩くと 404** ですが、**マガジンの hex key ** ( m... ) を入れると現在も普通に通ります (前述「記事をマガジンに紐付ける」参照)。 {id} のラベルが misleading なだけで、エンドポイント自体は今も生きています。当初本記事でも「現在は 404」と書いていましたが、これは私の検証時のパスの解釈ミスで、別途訂正しました。
GET/POST/DELETE /api/v1/note/{numeric_id}/comments (コメント API v1 系) — 2026 春から実質廃止
ego_station さんの一覧表をはじめ、ほぼ全ての先行記事に載っているこのコメント API ですが、2026 春ごろから挙動が変わって 常に空配列を返す ようになりました。404 や 410 ではなく 200 で空配列 ( [] ) を返してくる silent failure なので、リクエストが正しく通っているように見えて気付きにくい挙動です。
実際の note Web UI が叩いている path を観察したところ、 GET /api/v3/notes/{key}/note_comments に移行していました (詳細は前述「ソーシャル」節の v3 サブセクション参照)。path だけでなく、書込系で X-Note-Client-Code ヘッダや comment の AST 形式が要求されるようになるなど、要件もそこそこ変わっています。
2024〜2025 年に書かれた先行記事を参照する場合は、コメント API だけは v3 系に読み替える必要があります。
ハマりポイント
最後に、エンドポイントを順に叩いてみたときに引っかかった挙動をまとめます。
PATCH / PUT の多くが「フル payload」要求
publish ( PUT /api/v1/text_notes/{id} )、マガジン更新 ( PUT /api/v1/our/magazines/{key} )、プラン更新 ( PATCH /api/v2/circle/plans/{plan_key} ) は、いずれも差分更新ではなく 全フィールドを毎回送る 設計になっています。
たとえばマガジンの説明だけ変えたいケースでも、 { "description": "新しい説明" } だけを PUT すると 400 "name is missing" のように落とされます。事前に GET で現在の状態を取り直して、変更したいフィールドだけ差し替えてフル payload を再送する、というラウンドトリップが必須です。
公開後の note_draft 消失
有料記事の編集に使う data.note_draft は、publish 直後にしばらく null になる挙動を観察しました。直後に再編集しようとすると、 free_body と pay_body を復元するために必要な情報 (body の全文と separator UUID) が取れなくなり、再 PUT が組み立てられません。
具体的には、 GET /api/v3/notes/{key} の body は無料部分しか含まれず、 free_body と pay_body は両方 null になります。 ?draft=true を付けても、 note_draft 自体が null になっているケースがありました。
少し時間が経つと note_draft が復活する場合もあるので、編集系の自動化を組むときは「note_draft が null なら少し待ってリトライ」のようなパスを入れておくと安全です。
note エディタが許容するインライン HTML タグはかなり限定的
body HTML に何の inline タグを入れても通るのかが気になって、 <code> <em> <strong> <i> <u> <s> <mark> <kbd> <tt> <samp> <var> <small> <sub> <sup> <abbr> <span> あたりを順に試したところ、 POST /api/v1/text_notes/draft_save 経由で note サーバ側のサニタイザがその場で剥がしている ことが見えました。
生存するインラインタグ: <em> / <strong> / <s> / <code>
削除されるタグ: <i> / <u> / <mark> / <kbd> / <tt> / <samp> / <var> / <small> / <sub> / <sup> / <abbr> / <span> (class や data 属性ごと丸ごと消える)
<div> を送ると 段落構造ごと壊れる: 受け取ったあとに </p> + div の中身 + 新規 <p></p> のように分割されるため、本来の <p> block が崩れます
さらに <code> には固有の罠があって、API 経由で送ると 一時的に保持される のですが、note の Web エディタで再保存すると <code> タグごと削除される挙動でした。 <li><p><code>...</code></p></li> のように list item の中に入れると Web 上の描画も崩れるので、インラインコードを使いたい場合は <code> を出さずに プレーンテキストにフォールバック するのが安全策です ( <em> を流用する手もありますが、 italic と区別がつかなくなります)。
<span> 系も同様で、class や style を付けても全部剥がれるため、CSS 装飾の差し込み口にはなりません。
有料記事は unpublish できない
「過去に有料記事として販売実績がある記事」は POST /api/v2/notes/{key}/change_status で status: "draft" を送っても、403「下書きに戻すことができません」で弾かれます。値段を変えたい場合は新規記事を作り直すしかなく、公開を取り下げたい場合も soft delete ( DELETE /api/v1/notes/{numeric_id} ) しか手段がありません。
メンバーシップ作成の不可逆性
繰り返しますが、 POST /api/v2/circle (メンバーシップ作成) は 1 ユーザー 1 個・削除 API なしの不可逆操作 です。試し叩きで作ってしまうと、アカウントを消すまで「メンバーシップを持っていない状態」には戻せません。
幸い名前・説明は事後に更新可能なので、もし誤って作ってしまった場合は名前を「準備中」のような表記に変えて、プランを作成しない状態 (signup_enabled=false / そもそもプラン無し) で残置するのが現実的な対処になります。
プラン suspend は月末まで残る
POST /api/v2/circle/plans/{plan_key}/suspend は「即時削除」ではなく、月末まで停止予定のプランとして残ります。既存メンバーへの請求と閲覧期間が絡むため、停止申請から最大 1 か月、状態を持ち続けます。
定期購読マガジンの categories は 2 つ必須
有料 (定期購読) マガジン作成時の categories 配列は 2 つ必須 です (UI 上は 2 つの select が表示される)。有料 (単体) は 1 つ以上、無料は 0 個でも可、と販売モードによって最小数が変わります。
UI 上は disabled で気付けますが、API 直叩きだとエラーレスポンスからしかわからないので、定期購読マガジンを作る場合は必ず 2 つ以上指定する必要があります。
活用のヒント

エンドポイントの一覧を眺めると、note の Web UI が裏でしている処理 (画像の 2 段アップロード、有料記事編集時の note_draft 取り扱い、フル payload PATCH の組み立て、Cookie 認証の維持など) はそこそこ手数があります。 curl で都度叩いていると、認証や本文 HTML の組み立てを毎回書き直すことになって、個人利用でも結構しんどい量です。
自分は実用上の利便性として、下記のような構成をローカルだけで組んで使っています。
Rust 製のローカル HTTP サーバー (常駐 daemon): note への HTTPS 通信、 _note_session_v5 Cookie の永続化、画像 / 添付の 2 段アップロード、Markdown → block 構造化、 note_draft を経由した有料記事の編集サイクル、などを 1 か所に閉じ込めるレイヤー。スクリプト側は localhost にだけ HTTP で問い合わせれば済むようにする
Python / Node の薄い SDK: daemon を叩くだけのランタイム依存ゼロのクライアント。Python は urllib 、Node は標準 fetch のみで実装できるので、ワンタイムスクリプトに毎回入れても重くなりません
要するに「note への HTTP」を Rust daemon に閉じ込めて、AI Agent や CLI スクリプトはローカルの薄い API を叩くだけにする、という分割です。Markdown 入力からの block 自動変換 (  をローカル画像アップロードに、単独 URL 段落を embed に、 [label](file:///...) を添付ファイルに、など) もまとめて daemon 側で処理しています。
このツール群は 個人利用専用で、公開する予定はありません。note の非公式 API は規約・robots.txt 上グレーゾーンであること、低頻度・最小限の利用に留めるべきであること、仕様変更で頻繁に壊れること、を踏まえて、配布物として責任を負うのが難しいためです。
同じ要件で似たレイヤーを自分で組みたい方は、本記事のエンドポイントマップを起点に「daemon に Cookie・画像アップロード・block 整形を寄せて、SDK は localhost を叩く薄い HTTP クライアントにする」という分割を試してみると、個人利用の範囲でかなり便利になると思います。
ちなみに、この記事自体も、上記の daemon + SDK で書いて投稿しています。Obsidian で Markdown を書いて、Python の SDK で create_draft → save_draft → publish_draft を叩くだけで note の下書きが組み上がり、公開できるところまでローカルで完結する状態です。執筆体験は普通の Markdown ブログとほぼ同じで、note 特有の block 構造 (block id 付き段落・figure・separator など) は daemon が裏で組み立ててくれます。
「記事を書くためにツールを書いて、そのツールで記事を書く」という再帰的な dog-fooding になっていますが、実際にこういう自動化が回るくらいには非公式 API も使い物になる、ということでもあります。
参考にした先行記事
調査の出発点として以下の記事を参照しました。
ego_station さんの 2024 年版一覧表は、note の API を扱う上での定番リファレンスとして広く参照されている印象でした。
同じ内容がご本人のブログにも転載されています。
fuji1080 さんの 2025 年 7 月版。直近のスナップショットとして参考になりました。
nori_nw さんによる pay_body フィールドの発見記事。有料記事の本文がどう構造化されているかが分かります。
hagure_melon さんの 2020 年度版。歴史的な経緯を辿るのに便利です。
applikengo_25626 さんの観察記事。
今回はこれらを土台に、より新しい時期 (2026 年) の挙動と、当時の記事には載っていなかった範囲を追加しています。
おわりに
note の Web UI が叩いている API の現状を、自分が観察した範囲でまとめました。 /api/v3/... 系を中心としつつ、書き込み系は /api/v1/... や /api/v2/... に分散していて、命名規則の一貫性はそこまで強くない印象です。とくにマガジンの /my/... と /our/... の使い分けや、メンバーシップが /circle/... 配下に置かれている点は、観察してみないと気付きにくい構造でした。
非公式 API は仕様変更で塞がれることが前提なので、本記事の内容も時間とともに古くなる可能性があります。実際 2 年前の ego_station さんの一覧表のうち、いくつかのパスは既に動かなくなっていました。本記事を参考にされる場合も、実機で動作確認した上で利用してください。
note 運営の方々の運用に対する敬意も忘れずに、節度ある低頻度の利用を心がけたいところです。
更新履歴
2026-08-05: S3 presigned upload の仕様変更と、予約投稿 ( status: "reserved" ) を新規追記
画像アップロードの presigned_post レスポンスに STS 一時認証情報用の x-amz-security-token フィールドが追加されていることを確認。これを S3 への POST に含めないと 403 InvalidAccessKeyId で拒否される(旧実装は 8 fields 前提だったため、フィールド自体は返ってきているのに使わず黙って弾かれていた)
予約投稿は status: "published" + 未来の publish_at では 422 で拒否され、代わりに status: "reserved" という専用の値を送る必要があると判明。予約日時が来ると note 側が自動で published に遷移させる
予約投稿 ( status="reserved" ) は「publish payload で空 body の 500 を返す 4 つの罠」のうち slug トラップがより厳しく効くことも確認。 slug / note_key が両方空だと status="published" では 422 の構造化エラーで済むが、 status="reserved" だと本文なしの 500 になる
2026-08-02: GraphQL API ( graphql.note.com ) を新規追記
記事一覧の巡回中、 GET /api/v2/creators/{urlname}/contents が実在する記事の一部を返さない現象を確認。原因はページネーションのバグではなく、 exclude_from_creator_top: true (「クリエイターページに非表示」設定) の記事が一覧から除外される仕様だった
note.com の Web フロントエンドは記事一覧の取得を GraphQL に移行済みで、新着一覧相当の CreatorAllNotesPageQuery と、月別アーカイブが使う CreatorArchivesPageQuery の 2 クエリを確認。後者は非表示設定に関係なく全件返す
「記事まわりの API 一覧」に上記の注記と GraphQL 節を追加
__type introspection クエリで NoteListItem の全フィールドを確認したところ、 exclude_from_creator_top 相当のフィールドはこの GraphQL スキーマに存在しないと判明。月別アーカイブが非表示記事を返すのは「フラグを無視している」のではなく「そもそもこのスキーマにその概念が無い」ため、と訂正
GET /api/v2/creators/{urlname}/archives (月別アーカイブトップページが使う年次投稿件数サマリー REST API) に独立した見出しを設けた。GraphQL の CreatorArchivesPageQuery と組み合わせると全件を過不足なく取得できる
2026-07-11: note 内記事のネイティブ埋め込みが POST /api/v1/embed (v1) 経由だと判明
「埋め込み (YouTube / X など)」節では note 内記事の埋め込みも embed_by_external_api (v2) の service=external-article で代替できると書いていたが、これは OGP ベースの「外部記事カード」止まり。Web UI が実際に生成しているコンパクトな <iframe class="note-embed"> 形式は、v2 系とは別の POST /api/v1/embed ( embeddable_type: Note , height: 211 ) を経由していた
レスポンスの envelope も v2 と異なり、 data.embedded_content.key に 1 段ネストされている
観察は Chrome DevTools の network panel だけでは取りこぼしがあったため、「観察方法」節の fetch/XHR hook を Tampermonkey userscript化して note.com / editor.note.com に常駐させ、実際に Web UI の「埋め込み」ボタンを操作しながら捕獲した
2026-05-22: POST /api/v1/sessions/sign_in の破壊的変更を反映
5 月下旬から payload に ** g_recaptcha_response (reCAPTCHA v3 トークン) フィールドが必須化**。トークン無しで叩くと 2xx で返るが _note_session_v5 Cookie が発行されない
結果として email + password だけの自前スクリプトログインは事実上塞がれた
認証 API セクションに 追記 box を追加し、ブラウザ Cookie 取込 (libsecret + AES-128-CBC v11 復号) への移行が必要なことを記載
2026-05-17: タイトルと本文の見出しを軽微にリライト(内容の変更なし)
2026-05-16: コメント API の全面書き換え
v1 ( GET/POST/DELETE /api/v1/note/{numeric_id}/comments 系) は 2026 春から空配列を返すようになり、実 UI は v3 ( /api/v3/notes/{key}/note_comments ) に移行。本記事もこれに合わせて差し替え
書込系 (POST/PUT/DELETE) は X-Note-Client-Code ヘッダ (64 hex) と comment フィールドの AST 形式 ( {type:"root", children:[...]} )、 acknowledgement: false の 3 点が必須
編集用の PUT /api/v3/notes/{key}/note_comments/{comment_key} が存在することを確認 (v1 では存在を確認していなかった)
削除パスは数値 comment_id ではなく nc... 形式の comment_key を取る (記事 key の n... とは別系統)
POST body に parent_key を足すと返信投稿になる
レスポンス形の補足: data はオブジェクトではなく 配列 が直接入る (v1 用コードで data.comments を見ていた場合は壊れる)。user オブジェクトの ユニーク識別子は user.key (32 hex)、旧 v1 にあった numeric id は v3 では返らない (集計スクリプトで commenter の同一性判定をしている場合は user.key で吸収する)
あわせて「観察方法」節に 書込 API 観察用の拡張版 hook (ヘッダーまで localStorage に積む) を追記
2026-05-14: メンバーシップ・マガジン・ハッシュタグ周辺の発見を追加
circle_permissions の shape ( {kind: "circle" | "circle_plan", keys: [...]} の 2 モード)
マガジン記事追加 API の訂正: POST /api/v1/our/magazines/{key}/notes は hex key で叩けば現在も生きている (numeric id だと 404)。DELETE 側 ( /{note_key} を path 末尾) も併記
GET /api/v3/hashtags/{tag}/notes (タグに紐づく記事一覧、 order=popular|new|hot 、1 ページ 50 件)
GET /api/v2/hashtags/{tag} に relatedHashtags フィールドが含まれることを追記
draft_save は hashtags / magazine_ids / circle_permissions 等の publish メタを silently 無視する (200 で返るが永続化されない)
slug フィールドの vestigial 性 (API では受け付けるが URL ルーティングには効かない)
unpublish 403 の 4 カテゴリ詳細 (有料 / 販売実績 / メンバーシップ / 有料マガジン) と回避策 ( circle_permissions: [] で再 publish or 直接 DELETE)
2026-05-13: 検証で見えた publish 系の挙動 + 周辺の発見を追加
PUT /api/v1/text_notes/{id} の 空 body 500 を返す 4 つの罠 (image_keys / lead_form / line_add_friend / body_length / slug)
見出し画像のエンドポイント POST /api/v1/image_upload/note_eyecatch (MIME 明示必須)
note のサニタイザが許容するインライン HTML タグの実機検証 ( <em> / <strong> / <s> / <code> のみ生存、 <code> も Web 再保存で削除される)
検索 API の sort パラメータ ( popular / hot / new ) と context 値 ( hashtag / circle / noteForSale ) を追記
2026-05-12: 添付ファイル取得 API ( GET /api/v2/attachments/download/{hash} ) と、記事 body 内の <figure embedded-service="attachment"> から添付一覧を抽出する手順を追加
2026-05-11: 初版公開
