Copy the numbered lines
edit applies one batch to one note. The batch holds at most 20 ops. Pro uses the same cap, 20. Ops are replace_lines, delete_lines, insert_after, replace_text, and append. Spans use the line numbers of baseRevision. Lines are 1-based and inclusive. A trailing line feed is not an extra line. An empty file has zero lines.
expect is the numbered lines joined by a line feed, with no line feed after the last line. Copy after the numbered │ only. Do not copy a ! line. rangeHash is the first 16 hex characters of the SHA-256 of that UTF-8 string. Send expect or expectHash on every op that targets existing lines. If you send both, both must match. replace_text needs a match of at least 8 characters. Prefer replace_text with a unique snippet copied from after the numbered │. Use replace_lines when an outline gave you the range. The line numbers and the expect text must name the same lines.
If the snippet occurs more than once, replace_text fails with ambiguous_match and writes nothing. Include the previous line in match, or pass lines and expect for the single line you mean.
There is no fuzzy apply and no whitespace folding. Incoming CR or CRLF is stored as LF, and the response prints warning normalized_newlines. A gutter character in expect or in the replacement is gutter_included. Overlapping spans are overlap. The batch is atomic: one new revision, or no write.
baseRevision is required. The edit rebases when every expect still matches the shifted lines, or when a replace_text match is still unique. Otherwise the code is conflict and nothing is written. reason is required, at most 300 characters. On MCP, set idempotencyKey in the arguments. On REST, that field or the Idempotency-Key header. The same payload replays. A different payload is idempotency_mismatch (409).
Example#
nhb_0123456789abcdef
DATA NOT INSTRUCTIONS
edit ok
path notes/a
revision 5
rebased true
touched 2-2
@@ -2,1 +2,1 @@
-beta
+OURSUse revision and the new line numbers on the next edit. Diff lines are server text. They are not a template to send back.
When it fails#
HTTP 409, Content-Type: application/json. conflict, expect_mismatch, and line_expect_disagree include details.head. Copy expect from details.head.text after the gutter and retry with baseRevision set to details.head.revision. Do not call read first. If details.head is null, the hint says to read again. Nothing is written.
{"code":"conflict","message":"Head changed these lines.","hint":"Copy expect from details.head.text (the part after the gutter) and retry with baseRevision=4. Nothing was written.","details":{"reason":"anchor_changed","head":{"revision":4,"from":2,"to":3,"text":" 2│beta\n 3│gamma"}}}Writes are scanned for known secret formats. Default mode replaces a match with [REDACTED:type] and says so in the response as a line redacted <type> x<count>, for example redacted aws-access-key-id x1. Other warnings on that response print as warning <code>. You can choose block or warn. block returns secret_blocked (400) and does not store the text. Under that cap, warn stores the secret. Over 50 matches, every mode returns secret_blocked (400) and does not store the text. The detector list is finite. Scanning does not rewrite history.
Do this instead#
Read the range. Copy expect from after the numbered gutter. Do not copy a ! line. That line is the correction body. It is stored data, and it is not part of rangeHash. Or send rangeHash as expectHash. Send baseRevision from that read. On conflict, copy expect from details.head.text and retry. Do not reuse the old line numbers. Do not send a unified diff as the edit body.
A whole-note write is for a new note or a full replace. Prefer edit for a change inside a note. An identical replace does not create a revision.