Why Stripe's 18,000-character dispute evidence ceiling is measured in UTF-16 code units — and how naive automation overwrites AE notes
When a B2B SaaS or AI/API merchant receives a charge.dispute.created webhook for Reason Code 10.4 (fraudulent), staging Visa Compelling Evidence 3.0 (CE 3.0) historical charge pairs alongside 90-day product usage telemetry inside evidence[access_activity_log] is the highest-leverage defense available. Yet in production audits, two subtle string-handling mistakes repeatedly ruin automated dispute staging: measuring character budgets with the wrong encoding unit and clobbering existing human account-executive (AE) or support notes already drafted in the Stripe Dashboard.
In RevClamp's dispute staging engine (buildNonDestructiveDisputeDraft, governed by invariants INV-SAAS-03 and INV-SAAS-04), we enforce an explicit 18,000 UTF-16 code unit budget (MAX_ACCESS_ACTIVITY_LOG_CHARS = 18_000), wrap machine-generated CE 3.0 evidence inside deterministic delimiter markers (--- BEGIN REVCLAMP CE3 EVIDENCE --- / --- END REVCLAMP CE3 EVIDENCE ---), and hard-lock submit: false.
1. Why UTF-16 code units (.length) diverge from UTF-8 bytes (Buffer.byteLength) and Unicode code points
Stripe's Dispute Evidence API enforces field-length ceilings on text fields such as evidence[access_activity_log] and evidence[uncategorized_text]. When engineers budget 90 days of API telemetry logs in Node.js, Python, or Go, three distinct measurements get conflated:
- UTF-16 Code Units (
str.lengthin JavaScript): Every Basic Multilingual Plane (BMP) character (U+0000..U+FFFF) consumes1UTF-16 code unit, while astral-plane characters (U+10000..U+10FFFF, such as mathematical symbols or emoji in user-agent/workspace metadata) are encoded as a surrogate pair consuming2UTF-16 code units. - UTF-8 Bytes (
Buffer.byteLength(str, "utf8")): Multi-byte characters (such as box-drawing tables, em-dashes, or European merchant company names) consume2to4bytes per character over the wire. - Naive
.slice(0, maxChars)Truncation Traps: Slicing a string blindly at an arbitrary index can sever the tail end of a Visa CE 3.0 prior-charge table or split a UTF-16 surrogate pair in half (leaving a lone high surrogate0xD800..0xDBFFthat fails strict UTF-8/JSON serialization). By allocating a strict18,000UTF-16 code unit budget and reserving space for the closing delimiter marker before appending log lines, you guarantee zero mid-line or mid-surrogate corruption.
2. Why naive webhook handlers overwrite human AE & support notes
When a high-value B2B invoice is disputed, an Account Executive or Support Lead often opens the Stripe Dashboard immediately and types context into the dispute evidence fields (for example: “Customer signed annual MSA #2025-884; ticket #9102 resolved on Oct 12”). If your background webhook worker processes charge.dispute.created or charge.dispute.updated and writes evidence: { access_activity_log: generatedTelemetry } without reading the existing dispute object first, your automation permanently deletes the human notes.
To guarantee non-destructive updates across repeated webhook deliveries, RevClamp's buildNonDestructiveDisputeDraft inspects the existing field for --- BEGIN REVCLAMP CE3 EVIDENCE --- and --- END REVCLAMP CE3 EVIDENCE ---. If the markers already exist, only the inner block between the markers is replaced in-place (preserving merchant notes both above and below the block). If no markers exist yet, the delimited block is appended below the existing merchant notes.
3. Five Stripe dispute evidence staging failure modes vs. RevClamp guardrails
| Dispute Evidence Failure Mode | Root Technical Cause | Chargeback & API Impact | RevClamp Non-Destructive Guardrail |
|---|---|---|---|
| 1. UTF-16 Ceiling Overflow (>18,000 units) | Unbounded 90-day usage log dump exceeds character budget | Stripe returns 400 parameter_invalid_string_length or truncates CE 3.0 summary |
Enforces MAX_ACCESS_ACTIVITY_LOG_CHARS = 18000 UTF-16 code units with whole-line budgeting |
| 2. Human AE/Support Note Clobbering | Webhook worker blindly overwrites evidence[access_activity_log] |
Erases signed MSA references and support ticket notes drafted by finance | Splices only between --- BEGIN REVCLAMP CE3 EVIDENCE --- and --- END REVCLAMP CE3 EVIDENCE --- |
| 3. Incomplete Visa CE 3.0 Prior-Charge Pair | Only 1 prior charge or missing IP/device overlap in 120d-365d window |
Fails Visa CE 3.0 liability shift rubric on Reason Code 10.4 |
Requires priorMatchedCount >= 2 plus positive hasIpMatch || hasDeviceMatch |
| 4. Premature Irreversible Submission | Automation calls stripe.disputes.update with submit: true |
Locks dispute in acquiring bank portal before human finance review | Hard-locks submit: false (submitForcedFalse: true) across all API and MCP paths |
| 5. Cross-Customer Evidence Contamination | Shared corporate email alias or missing customer_id binding |
Attaches another tenant's IP/device logs to the disputed charge | Enforces positive expectedCustomerId binding (INV-SAAS-03) before log inclusion |
4. Lint your dispute payload in the browser, via CLI, or via MCP (revclamp_lint_dispute_evidence)
You can validate any candidate evidence[access_activity_log] payload against the 18,000 UTF-16 code unit ceiling, non-destructive delimiter markers, and Visa CE 3.0 prior-charge rubric without an API key using our free browser linter (/tools/stripe-dispute-evidence-char-counter), our 9th MCP tool (revclamp_lint_dispute_evidence on POST https://api.revclamp.com/api/mcp), or the revclamp-mcp CLI:
# 1. One-shot CLI lint of a non-destructive Visa CE 3.0 dispute evidence payload
npx -y github:revclamp/revclamp-mcp --lint-evidence valid_ce3_non_destructive
# 2. JSON-RPC 2.0 MCP tools/call payload (POST https://api.revclamp.com/api/mcp)
{
"jsonrpc": "2.0",
"id": "dispute-lint-1",
"method": "tools/call",
"params": {
"name": "revclamp_lint_dispute_evidence",
"arguments": {
"preset": "valid_ce3_non_destructive"
}
}
}