ホーム / お役立ち記事 / Notion API で長い文が 400 で拒まれる理由|2000字の上限と分けて送る方法

Knowledge

Notion API で​長い文が​ 400 で​拒まれる​理由|​2000字の​上限と​分けて​送る​方​法

Notion API で長い文を送ると HTTP 400 で拒まれるのは、rich text の1つの断片に入る文字が2000字までだからです。弊社の実測では、Notion はこの2000を UTF-16 の単位で数え、「𠮷」や試した絵文字は1字を2と数えました。200 が返っても黙って落ちる字もありました。分け方、落ちる字、読み戻しの確かめ方を、弊社の実測と公開の文書でまとめます。

著者: 村山 悠太(ゴムマリ) 最終更新: 2026年11月

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 の公開の文書で整理します。

原因: 1つの​断片は​2000字まで、​断片の​数は​100個まで

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 の語はありませんでした。

弊社の​実測: 2000は​ UTF-16 の​単位で​数えていた

弊社は、ほかのシステムや書類の文を 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の段落)。

  • 2000字の断片2つ: 200で入り、読み戻すと4000字の断片1つにまとまっていた(版2022-06-28と2026-03-11でも同じ)
  • 読み戻した4000字をそのまま送り返す: 400(text.content.length should be ≤ `2000`, instead was `4000`)
  • 太字の2000字と地の2000字: 読み戻しでも2つのまま
  • 英字2000字の断片100個: 200で、20万字が入った。101個は400(rich_text.length should be ≤ `100`, instead was `101`)
  • ひらがな2000字の断片100個: 送る本文が約604KBで、413(Request body too large)。50個(約302KB)は200

約604KBで返ったのは400でなく413で、文書の Status codes の表に413は載っていません。

テキストの列では、2000字の断片3つが、読み戻しでも3つのまま返りました。

黙って​落ちる​字: 200 が​返っても、​送った​文の​とおりとは​限らない

試す字の前後に「前」と「後」を置いた文を、段落とテキストの列に送りました。どれも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単位の字の途中で切れることがあります。

見分け方​: 応答の​番号と​ message で、​どこが​超えたかを​分ける

見え方 疑う所
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と数えるので、この差には出ません。

直し方​: UTF-16 で​数え、​字の​途中で​切らずに​分ける

手順は次のとおりです。1は弊社が制御文字の件で実際に通した直し方で、2〜4は今回の実測から組んだものです。

  1. 送る前に、Notion が落とす字を自分で落とす(読み戻しとの照合をそろえるため)
  2. UTF-16 の単位で2000以下の断片に分け、2単位の字・結合する字・絵文字のつなぎの途中では切らない
  3. 1つの段落の断片は100個まで、1回の要求の本文は500KBより小さくし、超える分は段落を分ける
  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 は、読み戻した段落を別の所へ写すときに使います。文字以外の断片(メンションや数式)はそのまま残しますが、その形で送り返せるかは確かめていません。

落とし穴: 実際に​踏んだ​もの

  • 図のコードが400で入らなかった2026年8月、mermaid の図のコードの、2000字をわずかに超える文を1つの断片で送り、400の validation_error になりました。2000字ごとに分けた配列で入れています。
  • ゼロ幅の空白で、照合が偽の不一致になった2026年7月、データベースの列に入れた文の読み戻しから、ゼロ幅の空白が消えていました。照合では、送る側の文も同じように落としてから比べる形に直しました。
  • PDF から取った文で、照合の約3分の2が不一致に見えた2026年10月、お客様の PDF の文を段落で入れたら、読み戻しの照合で項目の約3分の2が「違う」と出ました。違いは全部、PDF から取り出すときに混じった制御文字の並びでした。
  • 太字の印が、そのまま字で出た2026年9月、Markdown の原稿の ** を文字のまま入れ、お客様のデータベースの約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つです。

  • 元のデータ(PDF・Excel・ほかのシステム)に何が混じっているかが分からず、落ちる字や Markdown の印を事前に洗えない
  • 読み戻しの照合を、全部のページと列で回す仕組みが無い

外注する​場合の​判断基準

外注の分かれ目は、元のデータが多く、書いた値を全件で照合して受け入れる必要があるかです。外注するなら、見積もりを比べる前に次の3点を確かめてください。

  • 字を UTF-16 の単位で数え、字の途中で切らずに分ける作りか
  • 書いた後に読み戻し、全件で送った文と照合するか
  • 落ちる字と Markdown の印の扱いを決め、照合の前にそろえるか

ゴムマリでは、Notion のデータベースの設計と構築、既存のデータの移行、更新の決まりまでを、業務データベースの開発・構築(Notion 対応)として承ります。

既存のシステムとの連携が要る場合は、Notion API や Notion Workers での定期の同期も組み込みます。料金は初期30万円から、保守は月3万円から、期間は2週間からです。

進め方は相談からご連絡ください。

Notion で頼めることの全体は、Notion の構築と開発にまとめています。

次の​一歩

400 で止まった要求があれば、応答の message の instead was の数字と、len() で数えた字の数を比べてください。

そのうえで、書いた値を1つ読み戻して送った文と比べ、消えた字があれば送る前に落とす形にそろえます。

一括で登録して同じ行が2つできる件は、Notion API で一括登録すると重複行ができる理由にまとめています。そちらは、行を作るかどうかの判断の層です。

要求が 429 で止まる件は、Notion API が 429 で止まる理由にまとめています。そちらは、要求が通るまでの待ち方の層です。

関連記事: Notion API で一括登録すると重複行ができる理由/Notion API が 429 で止まる理由

この記事を書いた人

代表 村山悠太のポートレート

村山 悠太(ゴムマリ代表)

ワークスアプリケーションズで大手企業向けERPの営業と導入を担当し、外資系SaaS企業と国内大手放送局で販路と新規事業の立ち上げ、AI学習用データを扱うAIスタートアップ2社でマーケティングと営業を担った。2024年10月から生成AIの実装に入り、いまは大手企業の現場で、AIエージェントを要件定義から運用まで手がけている。

代表の経歴を見る

Ask

この​業務を​頼むなら

業種・規模を問わず

システム連携・API 開発

SAP® ソフトウェアや基幹システム、intra-mart、SharePoint などのデータを、Notion や CRM などほかのシステムへつなぎます。

初期 50万円〜(2週間〜)

システム連携・API 開発を見る

Related

関連

FAQ

よく​ある​質問

Notion API の2000字は、全角も半角も同じ数え方ですか?

弊社の実測では、ひらがなも英字も1字を1と数え、どちらも2000字まで通りました。「𠮷」や試した絵文字のように UTF-16 で2単位になる字は、1字を2と数えました。Notion の文書は、数え方を書いていません。

2000字より長い文は、Notion に入らないのですか?

入ります。2000字以下の断片に分け、配列で送ります。弊社の実測では、1つの段落に英字の2000字の断片を100個、計20万字が入りました。日本語で同じ量は送る本文が約604KBになり413で拒まれ、半分の10万字は通りました。

データベースのテキストの列も、同じ上限ですか?

弊社の実測では、列でも1つの断片が2001字だと400でした。2000字の断片を3つ送ると6000字が入り、読み戻しでも3つの断片のまま返りました。

200 が返れば、送った文のとおりに入っていますか?

入っていないことがあります。弊社の実測では、ゼロ幅の空白・一部の制御文字・復帰(CR)・行区切りは、200 が返ったまま落ちました。書いた後に読み戻し、送った文と比べると分かります。

読み戻した本文を、別のページへそのまま写せますか?

弊社の実測では、そのままでは写せませんでした。段落の読み戻しでは同じ書式の断片が1つにまとまり、4000字の断片をそのまま送ると400でした。写す前に、2000字以下に分け直します。

ご相談は​こちらから

相談は無料です。

相談する