Tutorial and full reference for Oeave's Agent API. Scoped keys, idempotency, async tasks, fiber and thread authoring, asset uploads, publish control.
What is an ecommerce agent API? A machine-readable HTTPS interface that lets AI assistants read and write product data on a brand's behalf, using scoped auth, idempotent writes, and async tasks. Oeave's Agent API ships this in full.
Oeave's Agent API is a write-first HTTP interface designed so that an AI agent — Claude, ChatGPT, or anything you build yourself — can author, revise, and publish a complete storefront on your behalf. This guide walks you from an empty project to a published Weave, then documents every endpoint in detail.
If you've only ever used the Oeave dashboard, think of the Agent API as a programmatic twin: every action a human can take — create a fiber, assemble a thread, import a style, publish — is callable over HTTPS with a scoped API key.
Base URL:
https://api.oeave.com/agent/v1Auth:
Authorization: Bearer oea_live_<prefix>_<secret>
The API is built around five primitives:
Every write endpoint is async-safe, idempotent, and returns a machine-readable error envelope.
Here's a complete round-trip: authenticate, list your projects, create a fiber, and watch it get embedded.
export OEAVE_API_KEY="oea_live_<prefix>_<secret>"
export BASE="https://api.oeave.com/agent/v1"
# 1. Who am I?
curl -sS "$BASE/me" -H "Authorization: Bearer $OEAVE_API_KEY"
# → { ok: true, data: { tier, scopes, usage, limits } }
# 2. List projects this key can access
curl -sS "$BASE/projects" -H "Authorization: Bearer $OEAVE_API_KEY"
# → { ok: true, data: [{ id, name, slug, … }] }
export PID="<project id from above>"
# 3. Create a fiber (202 — embedding runs in the background)
curl -sS -X POST "$BASE/projects/$PID/fibers" \
-H "Authorization: Bearer $OEAVE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-$(date +%s)" \
-d '{
"kind": "brandStory",
"data": {
"title": "Built for the fjords",
"richText": "<p>Hand-assembled in Bergen.</p>"
}
}'
# → { ok, data: { id, version }, pending: [{ taskId, kind }] }
# 4. Poll the task until it finishes
curl -sS "$BASE/tasks/<taskId>" -H "Authorization: Bearer $OEAVE_API_KEY"
# → { data: { status: "success", result: { indexed: 1 } } }
Once you've seen that round-trip work, the rest of this document is just "what other endpoints exist and what payloads do they take."
API keys are created from the Oeave dashboard at oeave.com/settings/ under API keys. Every key has:
fibers:write, threads:delete, publish:write. Grant the narrowest set the agent needs.Keys are always live — there's no separate test mode. Treat the secret like a password. It's shown once at creation; Oeave only stores its hash. If it leaks, revoke and reissue.
These are table stakes for the API. The sections that follow assume them.
Success:
{ "ok": true, "data": { /* resource */ }, "pending": [ /* optional */ ] }
Async creates and triggers return HTTP 202 with pending[] — a list of background tasks you can poll:
{
"ok": true,
"data": { "id": "k97…", "version": 1776870012036 },
"pending": [
{ "taskId": "nd7…", "kind": "embed_fiber_plan", "resource": "fibers/k97…" }
]
}
A Location header on 202 points at the first pending task's URL for convenience.
Failures are always:
{ "ok": false, "error": { "code": "validation", "message": "…", "details": { } } }
HTTP status matches the code:
| Code | HTTP | When it fires |
|---|---|---|
unauthorized | 401 | Missing or malformed Authorization header |
invalid_key | 401 | Key not found |
revoked / expired | 401 | Key no longer usable |
forbidden | 403 | Key not permitted on this project |
insufficient_scope | 403 | Scope missing; details.requiredScope names it |
not_found | 404 | Unknown resource, malformed id, or unmapped route |
validation | 400 | Bad or missing field; details explains |
conflict | 409 | If-Match version did not match |
rate_limited | 429 | Per-tier rpm/rph exceeded; Retry-After header |
quota_exceeded | 402 | Monthly quota exhausted; details.metric names it |
internal | 500 | Bug — report it |
Always branch on error.code, never on error.message (it's human-readable and may change).
POST requests that create or enqueue work accept an Idempotency-Key header. Send any stable string (a UUID, a hash of the payload, anything your agent can reproduce). Oeave dedupes for 24h — retrying with the same key returns the original result instead of creating a second fiber.
Every mutable resource has a version (equal to updatedAt in ms). When you PATCH, include If-Match: <version> to get a clean 409 if someone else changed the row since you read it. Without If-Match, PATCH is last-write-wins.
X-RateLimit-Limit and X-RateLimit-Remaining. On 429, honour Retry-After.assetUploads, embeddingTokens, publishBuilds, etc.). Every response carries X-Quota-<metric>-Remaining. A write that exhausts a quota returns 402 quota_exceeded with metric, used, limit, and resetsAt.Agents should not hardcode the endpoint list. Start every session by reading the live catalog:
# Canonical skill file — drop into Claude Code or any agent that reads markdown
curl https://api.oeave.com/agent/v1/skill
# JSON catalog of every endpoint, scope, fiber kind, and component shape
curl -sS "$BASE/docs" -H "Authorization: Bearer $OEAVE_API_KEY"
# Shape hints for every fiber kind
curl -sS "$BASE/docs/fiber-kinds" -H "Authorization: Bearer $OEAVE_API_KEY"
# Shape hints for every component kind
curl -sS "$BASE/docs/components" -H "Authorization: Bearer $OEAVE_API_KEY"
The /docs response is versioned. Refresh when version changes.
GET /projectsLists every project the authenticated user can access via this key. Honours the key's projectIds allowlist.
curl -sS "$BASE/projects" -H "Authorization: Bearer $OEAVE_API_KEY"
GET /projects/:pidFetches a single project. Returns { id, name, slug, description, createdAt, updatedAt, onboardedAt }.
Fibers are the atomic unit of content. Every fiber has a kind (one of 35+ shapes — brandStory, featureCard, technicalSpecification, video, productProfile, threeDModel, …) and a free-form data object matching that kind's schema. Call /docs/fiber-kinds to get required/optional fields and an example for each.
GET /projects/:pid/fibersList fibers on a project. Optional ?kind=<name> filter.
curl -sS "$BASE/projects/$PID/fibers?kind=testimonial" \
-H "Authorization: Bearer $OEAVE_API_KEY"
GET /projects/:pid/fibers/:fidFetch a single fiber. data is returned parsed (not a JSON string).
POST /projects/:pid/fibersCreate a fiber. Requires fibers:write.
curl -sS -X POST "$BASE/projects/$PID/fibers" \
-H "Authorization: Bearer $OEAVE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: fiber-$MY_STABLE_ID" \
-d '{
"kind": "featureCard",
"data": {
"title": "Dual-layer insulation",
"text": "24h cold, 12h hot.",
"icon": { "assetId": "as_icon_abc" }
}
}'
Returns 202 with the new fiber row and a pending embed_fiber_plan task. Indexing happens in the background; the fiber is usable (and searchable after the task completes) regardless.
PATCH /projects/:pid/fibers/:fidUpdate a fiber. Requires fibers:write. Two modes:
op: "merge" (default) — shallow-merge the new data into the existing data.op: "replace" — discard the existing data and use the new one.Optional headers:
If-Match: <version> — return 409 on concurrent edits.skipReembed: true — keep the existing embedding (useful for small cosmetic edits).curl -sS -X PATCH "$BASE/projects/$PID/fibers/$FID" \
-H "Authorization: Bearer $OEAVE_API_KEY" \
-H "Content-Type: application/json" \
-H "If-Match: 1776870012036" \
-d '{ "data": { "text": "24h cold, 16h hot." } }'
DELETE /projects/:pid/fibers/:fidSoft-deletes the fiber and removes its embeddings. Requires fibers:delete.
POST /projects/:pid/fibers/:fid/reembedExplicitly rebuild the fiber's embedding. Useful when upstream embedding settings change. Returns 202 with a pending task.
Anywhere a fiber wants an image, video, model, or font, pass { "assetId": "as_..." }. The server expands it into the full src shape (URL, metadata, CDN variants) on write. That means your agent never has to remember the exact src schema.
{ "imageSrc": { "assetId": "j57abc…" } }
Threads live on the project's primary product. They're ordered lists of components (carousels, CTAs, text blocks, 3D model embeds, forms, etc.). Threads are the surface a buyer actually sees when browsing a Weave.
GET /projects/:pid/threadsReturns the full thread list. Each thread has { id, title, slug?, summary?, funnelStage?, components[], createdAt, updatedAt }.
GET /projects/:pid/threads/:tidFetches a single thread.
POST /projects/:pid/threadsCreate a thread. Requires threads:write. Minimum body is { title }.
curl -sS -X POST "$BASE/projects/$PID/threads" \
-H "Authorization: Bearer $OEAVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "How it works",
"funnelStage": "consideration",
"summary": "From unboxing to first use.",
"components": [
{ "kind": "text", "richText": "<p>Three steps.</p>" }
]
}'
Components get server-generated ids if omitted.
PATCH /projects/:pid/threads/:tidMerge or replace thread fields. op: "replace" discards the existing components array.
DELETE /projects/:pid/threads/:tidRemoves the thread. Requires threads:delete.
POST /projects/:pid/threads/reorderReorders threads on the product.
{ "threadIds": ["t_a", "t_c", "t_b"] }
Any threads not listed are appended in their original order so nothing gets dropped by accident.
For finer-grained edits, you can append, patch, or delete components directly:
POST /projects/:pid/threads/:tid/components — body { component, position? }. Inserts at position (default: end).PATCH /projects/:pid/threads/:tid/components/:cid — body { patch, op? }.DELETE /projects/:pid/threads/:tid/components/:cid.Each call rewrites the thread's components[] atomically and bumps updatedAt.
Style is a { desktop, mobile } object covering colors, fonts, spacing, motion, layout, and brand graphics. mobile inherits any omitted field from desktop.
GET /projects/:pid/styleReturns the current style.
PATCH /projects/:pid/styleDeep-merges a style patch. Fonts and brand graphic images are { assetId } references — upload them first.
curl -sS -X PATCH "$BASE/projects/$PID/style" \
-H "Authorization: Bearer $OEAVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"desktop": {
"presentationMode": "feed",
"colors": {
"primary": { "r": 20, "g": 40, "b": 80, "a": 1 },
"mode": "dark"
}
}
}'
Schema is strict — extra fields return 400 validation with the offending field name.
POST /projects/:pid/style/import-from-urlScrapes colors, fonts, and logos from a live website and applies them. Returns 202 with a pending task.
curl -sS -X POST "$BASE/projects/$PID/style/import-from-url" \
-H "Authorization: Bearer $OEAVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://brand.com" }'
Metered against llmTokens.
Agents upload media in two steps — request a presigned URL, PUT the bytes, then finalize. Dedupe is by SHA-256.
POST /projects/:pid/assets/uploadsAsk for an upload URL. Body:
{
"fileName": "hero.jpg",
"contentType": "image/jpeg",
"sha256": "<hex sha256 of the file>"
}
Response is one of:
// First time seeing this hash — upload it
{
"data": {
"reused": false,
"uploadUrl": "https://…?X-Amz-…",
"r2Key": "…",
"publicUrl": "https://cdn.oeave.com/…",
"method": "PUT",
"headers": { "Content-Type": "image/jpeg" },
"expiresInSec": 600
}
}
// Already uploaded — reuse the existing asset row
{ "data": { "reused": true, "assetId": "j57…", "publicUrl": "…" } }
On a fresh upload, PUT the file bytes directly to uploadUrl (no Oeave auth header needed on the PUT — the presigned URL carries its own signature).
POST /projects/:pid/assets/finalizeAfter the PUT succeeds, tell Oeave about the new asset:
{
"r2Key": "…",
"publicUrl": "…",
"sha256": "…",
"fileName": "hero.jpg",
"contentType": "image/jpeg",
"fileSize": 842131,
"width": 1920,
"height": 1080,
"variants": ["depth"]
}
variants is optional. Right now "depth" is the only supported trigger — it queues a depth-map generation task if the asset is an image and the user has depth quota left.
GET /projects/:pid/assets / GET /projects/:pid/assets/:aidList (filter with ?kind=image|video|model|font|audio) or fetch one.
DELETE /projects/:pid/assets/:aidDeletes the asset row. The R2 object is retained (safer — published builds may reference it).
POST /projects/:pid/assets/:aid/depthRe-run depth-map generation. Requires assets:write and depth quota.
Vector search over embedded content.
# Over fibers
curl -sS "$BASE/projects/$PID/search/fibers?q=warranty+policy&limit=10" \
-H "Authorization: Bearer $OEAVE_API_KEY"
# Over threads
curl -sS "$BASE/projects/$PID/search/threads?q=how+it+works&limit=5" \
-H "Authorization: Bearer $OEAVE_API_KEY"
Each hit is { fiberId | threadId, chunkIndex, chunkText, score }. Agents can use this to avoid duplicating content they've already written, or to find the right fiber to patch instead of creating a near-duplicate.
GET /projects/:pid/publishPublish status: { productId, publishedAt, version, publishedUrl, pageUrl, llmTxtUrl, videoSitemapUrl }.
POST /projects/:pid/publishKicks off a full publish — builds the weave bundle, uploads to R2, purges Cloudflare cache, regenerates llms.txt and videos.xml. Metered against publishBuilds.
curl -sS -X POST "$BASE/projects/$PID/publish" \
-H "Authorization: Bearer $OEAVE_API_KEY"
# → 202 { productId, status: "publishing" }
Poll GET /projects/:pid/publish for publishedAt to update.
Async work that can't be done inline (embeddings, depth generation, style imports, publishes) runs as a task. Every endpoint that creates one returns its taskId in pending[].
GET /tasks/:id{
"data": {
"id": "nd7…",
"kind": "embed_fiber_plan",
"status": "queued" | "running" | "success" | "failed" | "cancelled",
"progress": 0.5,
"completedSteps": 2,
"totalSteps": 4,
"resource": "fibers/k97…",
"result": { "indexed": 3 },
"error": null,
"createdAt": 1776…, "startedAt": 1776…, "finishedAt": 1776…
}
}
Poll until status is terminal (success, failed, or cancelled). For long-running work, space polls a few seconds apart — there is no server-push yet.
GET /tasksRecent tasks for the authenticated user. ?status=<status>&limit=<n> filter.
POST /tasks/:id/cancelSignal cancellation. Running tasks best-effort stop at the next checkpoint.
Scopes are namespaced resource:action. Bundles:
agent:readonly — projects:read, fibers:read, threads:read, style:read, assets:read, publish:read, search:read, tasks:read, embeds:read.agent:full — everything above plus the write and delete variants.Full scope list: projects:read, fibers:read/write/delete, threads:read/write/delete, style:read/write, assets:read/write/delete, publish:read/write, search:read, tasks:read/write, embeds:read/write.
Given a brand description, spin up a thread from scratch.
# 1. Import a visual identity from the merchant's website (async)
curl -sS -X POST "$BASE/projects/$PID/style/import-from-url" \
-H "Authorization: Bearer $OEAVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://merchant.com" }'
# 2. While that runs, create the fibers you need
for KIND in brandStory featureCard testimonial pricing faq; do
curl -sS -X POST "$BASE/projects/$PID/fibers" \
-H "Authorization: Bearer $OEAVE_API_KEY" \
-H "Content-Type: application/json" \
-d "$(build_fiber_payload $KIND)"
done
# 3. Build a thread referencing the new fibers
curl -sS -X POST "$BASE/projects/$PID/threads" \
-H "Authorization: Bearer $OEAVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Why us", "components": [ … ] }'
# 4. Publish
curl -sS -X POST "$BASE/projects/$PID/publish" \
-H "Authorization: Bearer $OEAVE_API_KEY"
When a nightly job pulls fresh data, patch fibers instead of recreating them:
# Find the existing fiber
HITS=$(curl -sS "$BASE/projects/$PID/search/fibers?q=stainless%20steel%20spec&limit=1" \
-H "Authorization: Bearer $OEAVE_API_KEY")
FID=$(jq -r '.data.items[0].fiberId' <<< "$HITS")
# Fetch its version, then PATCH with If-Match
VER=$(curl -sS "$BASE/projects/$PID/fibers/$FID" -H "Authorization: Bearer $OEAVE_API_KEY" | jq -r '.data.version')
curl -sS -X PATCH "$BASE/projects/$PID/fibers/$FID" \
-H "Authorization: Bearer $OEAVE_API_KEY" \
-H "Content-Type: application/json" \
-H "If-Match: $VER" \
-d '{ "data": { "specs": [ … refreshed rows … ] }, "skipReembed": false }'
RESP=$(curl -sS -X POST "$BASE/projects/$PID/fibers" … )
CODE=$(jq -r '.error.code // empty' <<< "$RESP")
if [ "$CODE" = "quota_exceeded" ]; then
METRIC=$(jq -r '.error.details.metric' <<< "$RESP")
RESETS=$(jq -r '.error.details.resetsAt' <<< "$RESP")
echo "Blocked on $METRIC until $(date -d @$((RESETS / 1000)))"
# agent should back off, notify the human, or switch to cheaper operations
fi
/me to confirm the key parses.error.details.requiredScope tells you exactly which scope the key is missing. Issue a new key with that scope.projectIds allowlist.If-Match with a stale version. Re-fetch and retry.error.details.metric and resetsAt tell you which and when.Retry-After. If you see this often, your agent is too chatty; batch where possible.curl https://api.oeave.com/agent/v1/skillcurl https://api.oeave.com/agent/v1/docs