Skip to main content

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.

An item is the primitive. Every record in Marfa is an item, regardless of whether it represents a note, a book, a person, a photo, or a custom type registered by an app. Every item, regardless of type, has four layers.

System fields

Set by the server (or SDK in modes without a server), immutable from client request bodies. The Item schema in the API reference is the source of truth for field names, types, and nullability. A few fields carry semantics that are worth knowing before reading the spec:
  • id — UUIDv7. Time-sortable. Generated server-side.
  • timestamp vs created_attimestamp is the user-meaningful time (when a photo was taken, when an event occurred). created_at is when the item arrived in Marfa. timestamp defaults to created_at.
  • source — stamped from the credential server-side in server-synced mode and not forgeable. See Provenance.
  • source_id — optional. The upstream system’s stable identifier for this item — an RSS entry id, a Calendar event id, an external row id. Combined with the credential-stamped source, it forms the natural key behind the POST /items upsert behavior described below. The sync agent stores each file’s root-relative path here — see Folder structure for how that shapes folder queries and cross-device behavior.
  • version — starts at 1, increments on every update. Used for optimistic concurrency; PATCH /items/{id} takes it for conflict detection.
  • schema_version — which version of the type schema the item was validated against. See Types.
  • device, capture_latitude, capture_longitude — optional, client-supplied, preserved as written, not validated.
Each of these system fields is reserved across every type — a custom-type schema cannot redeclare them as properties. Registration is rejected 400 property_shadows_field. See Reserved field names for the full list and the rationale.

Properties

Type-schema-validated JSON. The item’s content — what makes one type different from another.
  • Schema-defined fields are enforced on write. Missing required fields produce a validation error.
  • Unknown fields are preserved — forward compatibility for schemas that evolve. Servers and SDKs must not strip unknown properties.
  • Properties are type-specific. A core.note has body; a core.media.book has title, author, isbn; their shapes don’t overlap.
See Types for the full core type set and how custom types work.

Metadata — the structural universal layer

Every item carries the metadata layer regardless of type. Three fields:
FieldTypePurpose
tagsarray of stringsFlat labels. Kebab-case. Reserved prefixes carry conventions (see Metadata).
tierenumlibrary or feed. The curated-library vs. high-volume-feed axis. See Tier.
stateenumactive / archived / trashed. See Lifecycle.
The metadata layer is closed — types can’t add to it. The three fields hold concepts with system-wide consequences: labels, the curation axis, and lifecycle. Additional always-present fields belong in properties on the type. The metadata layer applies only to items — edges and attachments don’t carry it.

Extensions

Namespaced JSON objects attached to an item. Extensions are sidecar state — not structured content with its own identity.
"extensions": {
  "readwise-reader.reading_progress": { "offset": 1234, "percent": 45 },
  "user.last_viewed": { "at": "2026-04-12T14:30:00Z" }
}
Extensions live at dedicated endpoints (/items/{id}/extensions/{namespace}) and respect per-namespace permissions. See Extensions for when to use them vs a custom item type.

Attachments

Every item has an attachments array. Each entry:
{
  "blob_ref": "sha256:abc123…",
  "mime_type": "image/png",
  "title": "original-filename.png",
  "role": "screenshot"
}
  • blob_ref — content-addressed reference (SHA-256). Storage deduplicates naturally; identical bytes have identical hashes.
  • mime_type — MIME type.
  • title — optional human-readable label.
  • role — optional. Well-known vocabulary: banner, screenshot, pdf, archive, readable, video, transcript, thumbnail, cover, original-html. Custom roles allowed in an app namespace.
Attachments vs primary blob. A core.file.* item carries its primary binary in a type-specific property (blob_ref is required on core.file). attachments[] is for secondary artifacts — a core.file.video might have attachments[] entries for a transcript and thumbnail; the video file itself sits in properties.blob_ref.

When to use attachments[] vs the attached-to edge

Both attach secondary content to an item, but they have different semantics. The choice turns on whether the secondary content has its own identity.
MechanismUse whenExample
attachments[] array on the itemThe artifact is bound to the host’s lifecycle (deleting the host deletes the attachment), it doesn’t need to be queryable as an item on its own, and a role describes what it is.A core.media.book with a cover image as attachments[{role: "cover", ...}]. The cover isn’t a core.file.image item in its own right — it’s a secondary artifact on the book.
attached-to edge (Edges)The artifact is its own item (a core.file.audio, a core.file.image), you want it queryable on its own (e.g. “find every image in this space”), or it might be linked from more than one host.An audio recording stored as a core.file.audio item, with an attached-to edge pointing at the host item. Both are first-class items; the edge models the relationship.
If in doubt: secondary content that only makes sense in the host’s context → attachments[]. Independent item that happens to belong to a host → attached-to edge.

Edges

Relationships to other items live as edges, not as fields on the item itself. about references, parent-child hierarchies, thread membership, annotations — all edges. See Edges. Single-item reads hydrate edges inline:
{
  "item": {
    "id": "...",
    "type": "core.note",
    "properties": { "body": "..." },
    "edges": {
      "about": { "edges": [...], "has_more": false },
      "parent-of": { "edges": [...], "has_more": false }
    }
  }
}
List reads opt into edges via ?include=edges.

Lists are lean, opt into extras

GET /items returns the minimum item shape by default — no edges, no metadata, no extensions. That’s deliberate: a naive “fetch the extras per item” loop is an N+1 trap. The ?include= parameter is the escape hatch: name what you need and the server batches it inline. Currently supported values (comma-separated):
  • ?include=edges — hydrates outbound edges per item, grouped by edge type. Same shape as single-item responses.
  • ?include=metadata — wraps each entry as { item, metadata } so tags travel inline.
  • ?include=extensions — hydrates extension namespaces per item, filtered by caller permissions (same rule as GET /items/:id/extensions).
Combinations work. ?include=edges,extensions is the typical “rendering a rich list view” shape: one request, everything the UI needs, no per-item follow-ups. The TypeScript SDK exposes typed helpers — client.items.listWithMetadata, client.items.listWithExtensions — that call these under the hood. Prefer them over assembling raw query strings.

Atomic item + edges write

A single POST /items call creates an item and its outbound edges in one transaction. See Edges for the request shape.

Natural-key upsert

POST /items is idempotent on (source, source_id). When both are present and resolve a non-trashed row in the caller’s space, the request short-circuits to update — properties shallow-merge, tags replace if provided, edges replace per-type if provided. Fields that only matter at create time (id, state, capture_*) are ignored on the update branch. The realised effect is reflected in the HTTP status:
  • 201 created — a new row was created. No matching (source, source_id) existed.
  • 200 ok — an existing row was updated via natural-key match. The response body is the updated item.
Without source_id, behavior is unchanged — every call creates a new row. With a source_id that resolves no row, the create path runs as before. The contract lets inbound integration handlers recover from whole-batch retries (handler succeeded, cursor write failed, the queue redelivers the same items) without producing duplicates. Same shape as the resource-level idempotency in mature integration platforms.

Item response shape

{
  "item": {
    "id": "019d9154-fc91-7d0b-ad4c-dcf3eb96ddbb",
    "type": "core.note",
    "tenant_id": "...",
    "properties": { "body": "...", "title": "..." },
    "tier": "library",
    "state": "active",
    "tags": ["work"],
    "timestamp": "2026-04-15T13:28:35.125Z",
    "created_at": "2026-04-15T13:28:35.125Z",
    "updated_at": "2026-04-15T13:28:35.125Z",
    "source": "My App",
    "version": 1,
    "schema_version": 1,
    "edges": { ... }
  }
}
The metadata layer fields (tags, tier, state) are flattened onto the item object. extensions are fetched separately via the extensions endpoints — or, on list reads, hydrated inline with ?include=extensions.