A publish acknowledges durability, not visibility
A200 from POST /v1/publish means the message is on durable media and survives a restart. It becomes searchable shortly afterwards.
There is no read-your-writes guarantee and no “wait until indexed” flag. A search that returns {"results": []} immediately after a publish is expected behaviour, not a failure. Run it again in a moment.
Acknowledgement modes
ack takes stored (the default) or durable.
Both acknowledge only after the message is written to durable storage, so choosing between them changes nothing today. The field is accepted so a future deployment can offer a weaker, faster mode without a breaking change.
Making a retry safe
A publish that times out may or may not have been stored.idempotency_key closes that gap:
The key is scoped to the namespace, so a retry is still recognised even when its embedding differs slightly from the first attempt.
On a
409, retrying as-is will keep failing. Either re-send the original message under that key, or publish the new message under a key of its own.
Identifying and ordering messages
A publish returns three values:message_id is stable and globally unique. It is the same identifier that comes back in search results and in match frames, and it is what you deduplicate on.
epoch and seq are opaque. Consecutive publishes to one namespace routinely return unrelated and non-increasing values, so seq is not a namespace-wide ordering. Do not use it to order messages, to detect gaps, or as a cursor. It is not a position you can pass to POST /v1/search. Order by publish-time metadata you attach yourself.
What a subscriber has to handle
Matches are delivered at-least-once. A consumer can see the same message twice, so deduplicate onmessage_id if a repeat would be harmful.
POST /v1/subscribe answers with a Server-Sent Events stream. The first frame confirms registration:
POST /v1/search when you need it:
message_id and score. Treat anything else in the frame as opaque.
The stream also carries keepalive comment frames — lines beginning with : — so an idle subscription is distinguishable from a dead one. Ignore them and never surface them as matches. Use a real SSE parser rather than splitting on newlines; comment-only lines and multi-line data: fields are where hand-rolled parsers break.
A subscription matches only messages published while it is open. It does not replay history. Pair it with a search if you also need what came before.
Setup failures and stream failures are different
A subscribe call that fails before the stream opens committed no state. Nothing was installed and no match can have been missed, so retrying the open is safe. Treat a transient code here exactly as you would on publish or search: honourretry_after_ms and reopen. See Errors and retries.
A stream that dies after delivering matches is a different problem. There is no resume cursor. Reconnecting registers a logically fresh subscription, and whether you see a replay or a gap across the reconnect is not guaranteed. Surface the disconnect to the caller with the last message_id seen, deduplicate on reconnect, and let the caller decide whether to reopen rather than reconnecting automatically.

