send_at on scheduled broadcastsAny PUT /v4/broadcasts/{id} request to a scheduled broadcast silently removes the scheduled send time. The broadcast reverts to "send now" state with no warning. This is a data-loss issue — the schedule disappears without any API error or confirmation.
Affected operations: updating content, changing the subject line, changing the sender — any PUT wipes the schedule.
Never use the API to edit scheduled broadcasts. Make all edits through the Kit UI only. If API edits are required, read the current send_at value first, then include it explicitly in your PUT payload to preserve it.
tel: links universally rejected by Kit's email editorThe Kit email editor rejects any anchor tag with href="tel:..." with the error "Unsupported value 'tel:5414611737' passed to href." This happens in both the visual editor and the Edit HTML modal — there is no way to insert a clickable phone link in a Kit email.
Use plain text for all phone numbers: 541-461-1737. Do not attempt tel:, tel:+1..., or any variant. Strip all tel: anchors from HTML before pasting into Kit.
All Kit API calls made via curl or Python from the Claude Cowork sandbox return exit code 56 (network unreachable / connection reset). The sandbox cannot reach api.kit.com. This eliminates all API-driven automation workflows for content updates and scheduling.
All Kit campaign operations must be performed via the Kit web UI using Claude's Chrome browser tools. Reserve API calls for read-only verification run from a local machine or external automation environment.
Kit's email editor is built on Slate.js with React. Direct manipulation of the DOM (innerHTML, textContent) updates what's visible on screen, but React's internal state still holds the old content. When the Save button is clicked, React serializes its own state — not the DOM — and the edit is lost or rejected.
This is why pasting content via innerHTML appeared to work visually but failed on save, and why a lingering tel: link continued to block saving even after it appeared to be removed from the visible editor.
Use document.execCommand('insertHTML', false, newHTML) instead of direct DOM manipulation. This command fires through the browser's editing pipeline, which Slate.js intercepts and uses to update its own React state. The edit survives saves and page navigations correctly.
execCommand('insertHTML') triggers Kit's Edit HTML modal as a side effectWhen document.execCommand('insertHTML', false, html) is called on Kit's Slate.js editor, it simultaneously updates React state correctly AND opens the "Edit HTML" modal overlay. If you click Save inside the modal, Kit validates the HTML again — which may differ from what was inserted.
Always click Cancel on the Edit HTML modal after calling insertHTML. The main editor already has the correct updated state. After canceling, make a trivial keystroke (space + backspace) to trigger React's onChange handler, then navigate to the Share page — Kit auto-saves on that transition.
type action sends literal Unicode escape sequencesThe Chrome MCP's type action types the literal characters ’ rather than the actual Unicode character (right single quotation mark '). This corrupts any email content with curly quotes, em dashes, or other non-ASCII characters.
Never use type for content with Unicode characters. Embed them directly in JavaScript string literals and inject via execCommand('insertHTML'):
// Works — Unicode in JS string literal, not typed document.execCommand('insertHTML', false, '<p>You’re getting this...</p>');
Kit's share page shows a scheduled broadcast's time with an "Unschedule" button but no pencil/edit button to modify just the time. To change the send time, you must: (1) click Unschedule to revert to "Sending now" state, (2) click the pencil icon that appears, (3) reselect the date and time from scratch in the react-datepicker.
Unschedule → pencil icon appears → click it → select date via find ref → select time via find ref. The find tool is more reliable than coordinate clicks for the date picker.
When navigating from the draft edit page to the share page (via the "Continue" button or direct URL), Kit automatically saves the current editor state. The "Saved" indicator in the bottom bar confirms the save. There is no need to click a separate Save button before leaving the editor.
After inserting HTML via execCommand and canceling the modal: make a no-op keystroke (space + backspace) to trigger React's onChange, then navigate to /share. Watch for the "Saved" indicator before proceeding to the schedule step.
The "From address" dropdown and other Kit UI controls do not reliably respond to coordinate-based clicks. The dropdown opens but options don't register, or the click lands outside the target. This is likely due to dynamic positioning and React portal overlays.
Use the Chrome MCP find tool to get element references, then click by ref rather than coordinate. Works consistently across dropdowns, date pickers, and overlay buttons.
When duplicating a CF7 form, the redirect action is not copied. Clicking the "Redirect" option in the "Add Action" dropdown via Chrome MCP JS click does not trigger the AJAX call that injects the action fields into the page. The dropdown closes but no fields appear.
POST the full CF7 admin form directly via fetch(form.action, {method:'POST', body: new FormData(form)}), appending wpcf7-redirect[actions][new_1][...] fields to FormData. This creates the action server-side. Reload the page and configure the newly created action ID normally.
When a CF7 form is duplicated, the new form ID is not automatically added to any existing WP Webhooks Pro webhook triggers. Form submissions from the duplicate silently bypass all Zapier zaps — no error, no log, no delivery.
After duplicating any CF7 form that feeds a webhook, immediately navigate to WP Webhooks Pro → Receive Data, find the relevant webhook, and add the new form ID to the wpwhpro_cf7_forms[] multi-select. Save and verify both form IDs appear on reload.
When trying to locate form inputs via document.elementFromPoint(x, y), the method returns null if the target element is above or below the current viewport — even if the element exists in the DOM. This caused failed element lookups on the CF7 admin form editor.
Call element.scrollIntoView() before using coordinate-based lookup. Once the element is within the viewport, elementFromPoint resolves correctly and inputs are fully interactive.
Kit V3 API broadcast content writes are silently ignored. POST /v3/broadcasts returns 201 but creates "New Broadcast" with null content. PUT /v3/broadcasts/{id} returns 200 but makes no changes. This was the original blocker for automated email sends throughout Q2.
Initial V4 testing also appeared broken — earlier tests with an older V4 key returned 200 but content did not persist. This led to the conclusion that the entire API write surface was unusable, which drove the manual paste workflow.
Root cause: The older V4 API key did not have write permissions for broadcast content. A new key ("Cowork Send Automation") created with correct scopes resolves the issue entirely.
PUT /v4/broadcasts/{id} with {"content": "...", "subject": "...", "send_at": "..."} writes all fields correctly when authenticated via X-Kit-Api-Key: kit_c6d81b86936b3b3c468b674cd1f3295c (Cowork Send Automation key). V3 remains broken — use V4 only.
Automation script: kit_send.py in ~/Documents/sperry-kit-automation/. Full V4, no manual paste required. Usage:
# Update existing broadcast (content + subject + schedule) python3 kit_send.py \ --broadcast-id 23982498 \ --subject "Enter to win + bring a friend" \ --html-file email_body.html \ --send-at "2026-05-06T13:00:00Z" # Create new broadcast from scratch python3 kit_send.py --new \ --subject "Your subject" \ --html-file email_body.html \ --send-at "2026-05-20T13:00:00Z"
Note on snippets: A V4 snippets workaround was explored simultaneously (Snippet ID 135163, "sperry_email_body"). Snippets also write correctly via V4. Kept as a backup mechanism but no longer needed for broadcast sends.
send_at fieldA broadcast visually confirmed as scheduled in the Kit UI (Share page shows date/time) can return "send_at": null from GET /v4/broadcasts/{id}. The API and the UI are out of sync.
Confirmed 2026-05-05: broadcast 23982498 was scheduled via UI for May 6 6am PDT but returned null send_at from the V4 API. The email sent correctly — Kit’s internal scheduling state is stored separately from what the API exposes.
Never rely on API send_at as ground truth for schedule status. Always confirm in the Kit UI. kit_send.py’s schedule-preservation logic reads send_at from the API — if the API returns null for a UI-scheduled broadcast, the preserved value will also be null. Do not run kit_send.py against a broadcast scheduled via the Kit UI without first verifying the API reports the schedule correctly via --preview.
| Broadcast | ID | Audience | Scheduled | Sender | Status |
|---|---|---|---|---|---|
| Email 1 — Pruning timing | 23610364 | Residential (tagged) | Apr 9 · 8:10 AM PDT | Michele | Confirmed |
| Email 2A — Pruning timing | 23610791 | Residential (tagged) | Apr 16 · 8:10 AM PDT | Michele | Confirmed |
| Email 2B — Commercial | 23610793 | Commercial | — | — | Deleted |
| Email 3 — 5 things to look for | 23610365 | All Club Members | Apr 23 · 8:10 AM PDT | Michele | Confirmed |
✓ API automation now available — As of May 5, 2026, kit_send.py handles broadcast content, subject, and scheduling end-to-end via the V4 API. The UI workflow below is kept for reference and edge cases, but programmatic sends are preferred. See Friction #13 for full details.
<a href="tel:..."> anchors — Kit will reject the entire edit if any are present.https://app.kit.com/campaigns/{id}/editfind("email body editor content area") → click by ref. The editor must have focus before execCommand will work.Cmd+A to select everything, then call execCommand('insertHTML', false, newHTML) to replace it. Unicode characters must be in JS string literals, not typed.insertHTML call triggers the modal as a side effect. Click Cancel — do not Save inside it. The main editor state is already correct./share. Wait for the "Saved" indicator. Content is now persisted.find refs for the From dropdown, date picker (day + time). Never use the API to update a scheduled broadcast — it clears the send time.