Three shapes cover every bulk flow.Documentation Index
Fetch the complete documentation index at: https://docs.myme.so/llms.txt
Use this file to discover all available pages before exploring further.
POST /items/bulk— list-in, items. The caller supplies explicit items to create or upsert. Replaces the historical/importendpoint.POST /items/bulk_action— filter-in, items. The caller supplies a filter and one action; the server applies it to every match.POST /edges/bulk— list-in, edges. The caller supplies explicit edges to create or upsert. Sibling to/items/bulkfor the graph half of tenant migrations.
POST /admin/restore-archive, ingests a tar.gz produced by GET /export?format=archive. It sits under Admin because it is a tenant-restore operation, not an item-level write.
Every bulk endpoint writes one aggregate audit entry per call and defaults
emit_events: false. Per-item or per-edge webhook fanout is an opt-in flag — bulk calls should not flood subscribers.Which endpoint do I want
I have a list of items
The caller holds the payload — an importer, a reconciler, a replay of an external source. Use
POST /items/bulk.I want to change every match
The target set is defined by a filter — “archive everything tagged wip”, “purge every trashed item older than 90 days”. Use
POST /items/bulk_action.I have a list of edges
Edges whose source and target exist already — replies, attachment links, a reply tree being migrated after its items landed. Use
POST /edges/bulk.Inline edges on items.bulk vs standalone edges.bulk
Both ways of writing edges alongside items exist. Pick based on whether source and target live in the same batch.| Situation | Endpoint |
|---|---|
| Edge source and target both in the same bulk-items payload | Inline edges block on /items/bulk |
| Edge source or target was created in an earlier batch (or already existed) | Separate /edges/bulk pass after items land |
| Mode-transition migration at tenant scale (Local → Marfa, iCloud → Marfa) | /items/bulk first, then /edges/bulk — cross-batch edges can’t ride inline |
POST /items/bulk
Admin-only. Up to 5000 items per call.
Modes
upsert(default) — match existing rows by(source, source_id)and update in place. Properties merge shallow-ly, last-writer-wins per key. Tags replace if provided. Edges union-merge if provided. Unprovided fields leave the existing row alone.create_only— matching rows surface asskipped, not updated. Preserves the old/importsemantic.
Atomic flag
atomic: true (default) wraps the batch in one transaction — any failure rolls back every write in the call. atomic: false runs per-item, returning per-item outcomes and continuing past errors.
Outcomes
Every response carries per-itemresults:
Outcome shape
Outcome shape
Inline edges
Each item may carry anedges block: a map from edge_type to target-item ids. Edges are created in the same transaction as the item, with replace-all semantics per edge type — any edge type listed wipes existing outbound edges of that type before the new set is created. Unmentioned edge types are untouched.
This path only works when source and target both land in the same call. For cross-batch edges — the target exists already, or lands in a separate bulk call — use /edges/bulk.
POST /edges/bulk
Admin-only. Up to 5000 edges per call.
Sibling to /items/bulk for the graph half of tenant migrations. Items created in an earlier bulk call exist on the server; a second pass through /edges/bulk wires them together without having to hold source and target in the same payload.
Modes
upsert(default) — match existing rows by(source_id, target_id, edge_type)and replacepropertiesin place. Non-matching rows are created.create_only— matching rows surface asskippedwith reason"duplicate_edge". Useful for replayable migrations where the same edge set may be pushed twice.
Validation
Every edge runs the same rules asPOST /edges: edge type registered, source and target exist and belong to the caller’s tenant, source and target type constraints satisfied, cardinality respected, no cycle on parent-of or supersedes. Violations surface per-edge in atomic: false mode or abort the batch in atomic: true mode.
Atomic flag
atomic: true (default) wraps the batch in one transaction — any failure rolls back every write in the call. atomic: false runs per-edge and returns per-edge outcomes.
Outcomes
Shape matches/items/bulk:
Outcome shape
Outcome shape
Events
emit_events: true fires an edge.created webhook per newly-created edge. updated outcomes emit nothing — the pubsub enum has no edge.updated variant; consumers that need to observe property replaces should re-fetch the edge after a bulk call.
POST /items/bulk_action
Applies one action to every item matching a filter. Six actions, discriminated on action.
action | Params | Auth | Confirm |
|---|---|---|---|
transition | state: "active" | "archived" | "trashed" | write-access per matched type | — |
purge | — | admin-only (hard 403) | "confirm": "PURGE" |
update_tags | add?, remove? (at least one non-empty) | write-access per matched type | — |
update_tier | tier: "library" | "feed" | write-access per matched type | — |
update_properties | patch: object (shallow-merged into properties) | write-access per matched type | — |
update_timestamp | timestamp: ISO 8601 | write-access per matched type | — |
write on — consistent with GET /items. purge is the exception and returns 403 for non-admin regardless of filter.
Filter grammar
Thefilter field accepts the same grammar as GET /items:
Safety rails
dry_run: truereturns matched ids and count without writing. Synchronous, returns 200.max_itemscaps the match set (default 10000, hard ceiling 50000). Going over returns400 bulk_cap_exceeded.confirm: "PURGE"is required on the purge action. Missing it returns400 bulk_confirmation_required.Idempotency-Keyheader (optional) — retrying aPOSTwith the same key returns the original job, not a duplicate. Scoped per tenant.
Async behaviour
Non-dry-run calls return 202 Accepted with a job envelope. The server queues the work and an in-process worker runs it through to a terminal status. PollGET /items/bulk_action/jobs/{id} until status is completed, failed, or cancelled. SDKs (TypeScript, Swift) hide the polling — client.items.bulkAction(input) resolves with the same BulkActionResult shape regardless of size.
DELETE /items/bulk_action/jobs/{id}. The worker observes the flag between chunks (typically within seconds) and stops; partial progress (processed / succeeded / errored) is preserved on the row.
Job rows live for 7 days after they reach a terminal state, then a sweep deletes them. Polling a long-gone job id returns 404 bulk_job_not_found.
Wiping a scratch tenant
Archive restore
POST /admin/restore-archive accepts a tar.gz produced by GET /export?format=archive. The body is application/gzip; the response is JSON counts.
(source, source_id); blobs are content-addressed and register idempotently. Tenant migration between Marfa instances is export-then-restore.