ホーム / お役立ち記事 / Notion API で長い文が 400 で拒まれる理由|2000字の上限と分けて送る方法
Knowledge
Notion API で長い文を送ると HTTP 400 で拒まれるのは、rich text の1つの断片に入る文字が2000字までだからです。弊社の実測では、Notion はこの2000を UTF-16 の単位で数え、「𠮷」や試した絵文字は1字を2と数えました。200 が返っても黙って落ちる字もありました。分け方、落ちる字、読み戻しの確かめ方を、弊社の実測と公開の文書でまとめます。
Notion API で長い文を送ると、HTTP 400 の validation_error で拒まれることがあります。rich text の1つの断片(text.content)に入る文字が、2000字までだからです。
長い文は、2000字以下の断片に分けて、配列で送れば入ります。弊社の実測では、1つの段落に2000字の断片を100個、英字で計20万字が入りました。
気をつけるのは字の数え方です。弊社の実測では、Notion は UTF-16 の単位で数えていました。「𠮷」や試した絵文字は1字を2と数えたので、Python の len() で2000字以下でも400になることがあります。
400 にならずに字が消えることもあります。弊社の実測では、ゼロ幅の空白や一部の制御文字は、200 が返ったまま落ちました。「𠮷」の途中で断片を割ったときも、その字が消えました。
この記事は、Notion とほかのシステムをつなぐ仕組みを任されている業務の担当者・情報システムの担当者に向けて書きます。コードは、仕組みを作る人にそのまま渡せる形で載せます。
原因と直し方を、弊社の実測(2026年11月2日)と、Notion の公開の文書で整理します。
Notion の文書「Request limits」の表と、その前の一文は、上限を次のように書いています。
| 上限 | 文書の記載 |
|---|---|
| rich text の1つの断片の文字 | 2000 characters |
| rich text やブロックの配列 | 100 elements |
| 1回の要求の本文 | ブロックの要素1000・全体で500KB |
同じ節は、上限を超えた要求には400の validation_error が返る、と書いています。
文書の注は、これが1回の要求の大きさの上限で、プロパティが持てる量の上限ではない、と書いています。
文書は「2000 characters」の数え方を書いていません。
2026年11月2日に読んだ Request limits・Rich text・Append block children・Update page の4頁に、UTF-16・code point・grapheme の語はありませんでした。
弊社は、ほかのシステムや書類の文を Notion へ書き込む仕組みを作っています。数え方を確かめるため、弊社の Notion の試しのページへ送りました。
| 項目 | 内容 |
|---|---|
| 実測の日 | 2026年11月2日 |
| API の版(Notion-Version) | 2025-09-03(一部を2022-06-28と2026-03-11でも) |
| 送った所 | ページの段落と、データベースのテキストの列 |
| 後始末 | 試しのページは、終わった後にゴミ箱へ入れた |
1つの断片だけの段落を送った結果です。Notion が数えた長さは、拒んだ応答の message の instead was の数字です。
| 送った文 | Python の len | Notion が数えた長さ | 結果 |
|---|---|---|---|
| 英字2000字 | 2000 | (通った) | 200 |
| 英字2001字 | 2001 | 2001 | 400 |
| 「𠮷」1000字 | 1000 | (通った) | 200 |
| 「𠮷」1001字 | 1001 | 2002 | 400 |
| 英字1999字と絵文字1つ | 2000 | 2001 | 400 |
| 結合文字の濁点で書いた「が」1001字 | 2002 | 2002 | 400 |
ひらがな2000字は、UTF-8 で6000バイトですが通りました。数えているのはバイトでも、見た目の字の数でもありませんでした。
「𠮷」と試した絵文字(U+1F44D)は、UTF-16 で1字が2単位の字です。Python の文字列は Unicode の code point の並びなので、len() はこれらを1と数え、Notion の数より小さくなります。
英字2001字・「𠮷」1001字・「𠮷」2000字・「𠮷」2001字は、版2022-06-28と2026-03-11でも400でした。
テキストの列でも、2001字の断片は400でした。
配列の結果です(版2025-09-03の段落)。
text.content.length should be ≤ `2000`, instead was `4000`)rich_text.length should be ≤ `100`, instead was `101`)Request body too large)。50個(約302KB)は200約604KBで返ったのは400でなく413で、文書の Status codes の表に413は載っていません。
テキストの列では、2000字の断片3つが、読み戻しでも3つのまま返りました。
試す字の前後に「前」と「後」を置いた文を、段落とテキストの列に送りました。どれも200が返り、段落と列で同じ結果でした。
| 字 | 結果 |
|---|---|
| ゼロ幅の空白(U+200B) | 落ちた |
| 制御文字の ESC・BEL・SUB・NUL | 落ちた |
| 復帰(CR・U+000D) | 落ちた。CRLF は改行(LF)だけが残った(CRLF は段落だけで試した) |
| 行区切り(U+2028) | 落ちた |
| タブ・改行(LF) | 残った |
| 制御文字の DEL・NEL | 残った |
| ZWJ・ZWNJ・WORD JOINER・BOM・ソフトハイフン | 残った |
| 結合文字の濁点・異体字セレクタ・IVS | 残った。濁点は、合成済みの「が」に置き換えられなかった |
Markdown の太字の ** |
文字のまま残った |
制御文字でも、落ちる字と残る字がありました。文書に決まりが無いので、この表は試した字だけの結果です。
もう1つ、断片を UTF-16 の単位で割り、「𠮷」の2単位を2つの断片に分けて送りました。200が返り、段落でも列でも「𠮷」だけが消えていました。
Python の文字列を切る分には、この割れ方は起きません。JavaScript の文字列の length は UTF-16 の単位なので、その単位で切ると、2単位の字の途中で切れることがあります。
| 見え方 | 疑う所 |
|---|---|
400・text.content.length should be ≤ `2000` |
1つの断片が2000を超えた |
400・rich_text.length should be ≤ `100` |
1つの rich_text の配列の断片が100個を超えた |
413・Request body too large |
1回の要求の本文が大きすぎる |
| 自分では2000字以下に数えたのに400 | 数え方の違い(UTF-16 の2単位の字や結合文字) |
| 200 なのに、読み戻しが送った文と違う | 黙って落ちる字か、字の途中で割った断片 |
instead was の数字が、Python の len() で数えた字の数より大きければ、UTF-16 で2単位の字が混じっています。結合文字は len() でも2と数えるので、この差には出ません。
手順は次のとおりです。1は弊社が制御文字の件で実際に通した直し方で、2〜4は今回の実測から組んだものです。
次のコードは、Notion API を Python で直接呼ぶときの部品です。標準のライブラリだけで動きます。
import json
import unicodedata
import urllib.request
API = "https://api.notion.com/v1"
HDRS = {"Authorization": "Bearer <トークン>", "Notion-Version": "2025-09-03",
"Content-Type": "application/json"}
LIMIT = 2000 # 1つの断片の上限(Notion は UTF-16 の単位で数えた)
MAX_PIECES = 100 # 1つの rich_text の配列に入る断片の数
MAX_BYTES = 450_000 # 1回の要求の本文の目安(文書の上限 500KB より小さく置いた値)
ZWJ = "\u200d"
def u16(s):
"""Notion の数え方(UTF-16 の単位)での長さ。"""
return len(s.encode("utf-16-le")) // 2
def clean(s):
"""弊社の実測で落ちた字を、送る前に落とす。CRLF は LF に、ほかの制御文字はタブと改行を残して外す。"""
s = s.replace("\r\n", "\n").replace("\u200b", "").replace("\u2028", "")
return "".join(c for c in s if c in "\t\n" or unicodedata.category(c) != "Cc")
def _joins_prev(s, i):
"""s[i] が前の字とつながって1字に見えるなら True(ここでは切らない)。"""
c, p = ord(s[i]), s[i - 1]
if p == ZWJ or c == 0x200D or unicodedata.category(s[i]) in ("Mn", "Mc", "Me"):
return True # 絵文字のつなぎ(ZWJ)の前後・結合文字・異体字セレクタ
if 0x1F3FB <= c <= 0x1F3FF or 0xE0020 <= c <= 0xE007F:
return True # 肌の色の修飾・旗のタグ
if 0x1F1E6 <= c <= 0x1F1FF: # 国旗は地域の字2つで1つ
n = 0
while i - 1 - n >= 0 and 0x1F1E6 <= ord(s[i - 1 - n]) <= 0x1F1FF:
n += 1
return n % 2 == 1
return False
def split_text(s, limit=LIMIT):
"""s を、UTF-16 で limit 以下の断片に分ける。字の途中では切らない。"""
pieces, start, size, safe = [], 0, 0, 0
for i, ch in enumerate(s):
if i > start and not _joins_prev(s, i):
safe = i # ここ(s[i] の前)なら切ってよい
w = 2 if ord(ch) > 0xFFFF else 1
if size + w > limit:
cut = safe if safe > start else i # 切ってよい位置が無いときだけ code point の境で切る
pieces.append(s[start:cut])
start, safe = cut, cut
size = u16(s[start:i])
if size + w > limit: # つながる字が limit より長い
pieces.append(s[start:i])
start, safe, size = i, i, 0
size += w
if start < len(s):
pieces.append(s[start:])
return pieces
def rich_text(s, **annotations):
"""テキストの列などに入れる rich_text の配列(clean した後の文)。100個を超えるなら止める。"""
rts = []
for p in split_text(clean(s)):
o = {"type": "text", "text": {"content": p}}
if annotations:
o["annotations"] = annotations
rts.append(o)
if len(rts) > MAX_PIECES:
raise ValueError(f"断片が {len(rts)} 個で、1つの rich_text に入る {MAX_PIECES} 個を超える")
return rts
def _size(obj):
return len(json.dumps(obj, ensure_ascii=False).encode("utf-8"))
def paragraphs(s):
"""長い文を段落のブロックの並びにする。1つの段落は断片100個まで、大きさも MAX_BYTES まで。"""
blocks, cur = [], []
for p in split_text(clean(s)):
piece = {"type": "text", "text": {"content": p}}
if cur and (len(cur) == MAX_PIECES or _size(cur + [piece]) > MAX_BYTES - 1000):
blocks.append({"type": "paragraph", "paragraph": {"rich_text": cur}})
cur = []
cur.append(piece)
if cur:
blocks.append({"type": "paragraph", "paragraph": {"rich_text": cur}})
return blocks
def api(method, path, body=None):
data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None
req = urllib.request.Request(API + path, data=data, headers=HDRS, method=method)
with urllib.request.urlopen(req) as r:
return json.loads(r.read())
def append_text(page_id, s):
"""長い文をページの末尾へ段落で足す。1回の要求はブロック100個まで、大きさも MAX_BYTES まで。"""
batch = []
for b in paragraphs(s):
if batch and (len(batch) == 100 or _size({"children": batch + [b]}) > MAX_BYTES):
api("PATCH", f"/blocks/{page_id}/children", {"children": batch})
batch = []
batch.append(b)
if batch:
api("PATCH", f"/blocks/{page_id}/children", {"children": batch})
def rechunk(rts):
"""読み戻した rich_text を、送り返せる形に分け直す(書式とリンクはそのまま)。"""
out = []
for r in rts:
if r.get("type") != "text":
out.append({k: r[k] for k in ("type", r["type"], "annotations") if k in r})
continue
link = r["text"].get("link")
for p in split_text(r["text"]["content"]):
o = {"type": "text", "text": {"content": p, **({"link": link} if link else {})}}
if "annotations" in r:
o["annotations"] = r["annotations"]
out.append(o)
return out
def verify(sent, rts):
"""送った文と、読み戻した rich_text をつないだ文を比べる。違えば最初に違う位置を返す。"""
want, got = clean(sent), "".join(r.get("plain_text", "") for r in rts)
marks = [m for m in ("**", "__", "`") if m in got]
if want == got:
return True, f"一致 {u16(got)} 単位" + (f"・Markdown の印が文字のまま {marks}" if marks else "")
i = next((k for k, (a, b) in enumerate(zip(want, got)) if a != b), min(len(want), len(got)))
return False, f"{i} 字目から違う: 送った {want[i:i + 5]!r} / 読み戻し {got[i:i + 5]!r}"
clean は、弊社の実測で落ちた字を送る前に落とします。Notion が残した DEL と NEL も、残ることを当てにせず外しています。
split_text は、UTF-16 の単位で数え、2000を超える手前の切ってよい位置で切ります。結合文字の濁点や絵文字のつなぎの前でも切りません。
弊社の実測では、濁点の前で割っても字は消えませんでしたが、テキストの列では「か」と濁点が別の断片で返りました。断片ごとに扱う処理で別の字に見えないよう、切らない形にしています。
paragraphs と append_text は、1つの段落の断片を100個まで、1回の要求を450KBまでに抑えます。450KBは、文書の500KBより小さく弊社が置いた値です。
約302KBと約604KBの間のどこで拒まれるかは、確かめていません。
本文が大きい要求は、Cloudflare の403で止まることもあります。弊社が当たった件と、gzip で圧縮して送る直し方は、Notion API が 429 で止まる理由にまとめています。
rechunk は、読み戻した段落を別の所へ写すときに使います。文字以外の断片(メンションや数式)はそのまま残しますが、その形で送り返せるかは確かめていません。
validation_error になりました。2000字ごとに分けた配列で入れています。** を文字のまま入れ、お客様のデータベースの約160ページの先頭の段落に ** が出ました。plain_text にも残るので、文の照合では一致に見えていました。PDF の件は、送る側でも制御文字を落としてから送る形に直しました。太字の件は、** で文を分け、太字の部分に bold の書式を付ける形に直しました。
書いた後に、ブロックや列を読み戻し、断片の plain_text をつないだ文を、送った文と比べます。上のコードの verify は、clean した後の送った文と比べ、違えば最初に違う位置を返します。
段落の読み戻しは、同じ書式の断片が1つにまとまるので、断片の数や区切りでは比べません。
verify は、一致しても ** などの Markdown の印が文字のまま残っていれば知らせます。
上のコードは、2026年11月2日に Python 3.14.6 で、実測の動きを真似た作り物の Notion で確かめ、本物の API には送っていません。
確かめたのは、実測で200だった文が1つの断片のまま、400だった文が2つ以上に分かれること、日本語の24万字を413にならない大きさで送れることです。
つながる字を割らないこと、rechunk の後に送れること、字の途中で割って消えた字を verify が見つけることも見ました。
1つの仕組みが、決まった所へ短い値を書くだけなら、自力で足ります。上の split_text と verify を、書き込みの1か所に入れます。
自力の壁は次の2つです。
外注の分かれ目は、元のデータが多く、書いた値を全件で照合して受け入れる必要があるかです。外注するなら、見積もりを比べる前に次の3点を確かめてください。
ゴムマリでは、Notion のデータベースの設計と構築、既存のデータの移行、更新の決まりまでを、業務データベースの開発・構築(Notion 対応)として承ります。
既存のシステムとの連携が要る場合は、Notion API や Notion Workers での定期の同期も組み込みます。料金は初期30万円から、保守は月3万円から、期間は2週間からです。
進め方は相談からご連絡ください。
Notion で頼めることの全体は、Notion の構築と開発にまとめています。
400 で止まった要求があれば、応答の message の instead was の数字と、len() で数えた字の数を比べてください。
そのうえで、書いた値を1つ読み戻して送った文と比べ、消えた字があれば送る前に落とす形にそろえます。
一括で登録して同じ行が2つできる件は、Notion API で一括登録すると重複行ができる理由にまとめています。そちらは、行を作るかどうかの判断の層です。
要求が 429 で止まる件は、Notion API が 429 で止まる理由にまとめています。そちらは、要求が通るまでの待ち方の層です。
ワークスアプリケーションズで大手企業向けERPの営業と導入を担当し、外資系SaaS企業と国内大手放送局で販路と新規事業の立ち上げ、AI学習用データを扱うAIスタートアップ2社でマーケティングと営業を担った。2024年10月から生成AIの実装に入り、いまは大手企業の現場で、AIエージェントを要件定義から運用まで手がけている。
Ask
業種・規模を問わず
システム連携・API 開発
SAP® ソフトウェアや基幹システム、intra-mart、SharePoint などのデータを、Notion や CRM などほかのシステムへつなぎます。
初期 50万円〜(2週間〜)
Related
Excel の台帳を Notion へ移すには、表の形とキーを決め、型を決めてリレーションと一緒に取り込み、数式と集計を作り直してから、件数・キー・値・先頭のゼロ・空の値・リレーション・計算の結果の7つで照合します。Notion の公式ヘルプと API の文書の決まりと、弊社が管理表の約2千明細を移し、Notion で計算した残が移したすべてのロットで元の表と一致した照合の手順でまとめます。
活用ガイドNotion AI に書類を作らせて照合するときは、手順を Notion AI への指示書、読めば決まる処理を添付のコード、1回ごとの結果を記録のデータベースに分けます。弊社の導入の事例の試作では、同じ題材で、文章の指示は発注番号を3回のうち2回誤り、添付のコードは2回とも誤りませんでした。分け方の問い、進める手順、誤りを仕込む照合の進め方を、弊社の記録でまとめます。
FAQ
弊社の実測では、ひらがなも英字も1字を1と数え、どちらも2000字まで通りました。「𠮷」や試した絵文字のように UTF-16 で2単位になる字は、1字を2と数えました。Notion の文書は、数え方を書いていません。
入ります。2000字以下の断片に分け、配列で送ります。弊社の実測では、1つの段落に英字の2000字の断片を100個、計20万字が入りました。日本語で同じ量は送る本文が約604KBになり413で拒まれ、半分の10万字は通りました。
弊社の実測では、列でも1つの断片が2001字だと400でした。2000字の断片を3つ送ると6000字が入り、読み戻しでも3つの断片のまま返りました。
入っていないことがあります。弊社の実測では、ゼロ幅の空白・一部の制御文字・復帰(CR)・行区切りは、200 が返ったまま落ちました。書いた後に読み戻し、送った文と比べると分かります。
弊社の実測では、そのままでは写せませんでした。段落の読み戻しでは同じ書式の断片が1つにまとまり、4000字の断片をそのまま送ると400でした。写す前に、2000字以下に分け直します。