Agent API — letting AI build your storefront

Tutorial and full reference for Oeave's Agent API. Scoped keys, idempotency, async tasks, fiber and thread authoring, asset uploads, publish control.

O
Oeave Team
April 22, 2026
15 min read

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/v1

Auth: Authorization: Bearer oea_live_<prefix>_<secret>

Who this guide is for

  • Merchants who want an agent to keep their store current. Point Claude Code at a project, describe what needs to change, and let it edit.
  • Developers integrating Oeave into an agent framework. MCP tools, Claude Agent SDK projects, or bespoke orchestrators.
  • Partners syncing Oeave with another system. A PIM, a CMS, a headless commerce backend that needs to push content into Oeave on a schedule.

The API is built around five primitives:

  • Projects — the top-level workspace you own.
  • Fibers — individual pieces of content (an image, a FAQ entry, a pricing block).
  • Threads — ordered arrangements of components on the product you're publishing.
  • Assets — media files uploaded to R2 and referenced from fibers.
  • Tasks — a queue of async work (embeddings, depth maps, publish builds) the server runs for you.

Every write endpoint is async-safe, idempotent, and returns a machine-readable error envelope.

60-second quickstart

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."

Generating an API key

API keys are created from the Oeave dashboard at oeave.com/settings/ under API keys. Every key has:

  • A scope list — e.g. fibers:write, threads:delete, publish:write. Grant the narrowest set the agent needs.
  • An optional project allowlist — if set, the key can only touch listed projects.
  • An optional expiry — for short-lived keys issued to an agent session.

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.

Conventions every endpoint follows

These are table stakes for the API. The sections that follow assume them.

Response envelope

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.

Errors

Failures are always:

{ "ok": false, "error": { "code": "validation", "message": "…", "details": { } } }

HTTP status matches the code:

CodeHTTPWhen it fires
unauthorized401Missing or malformed Authorization header
invalid_key401Key not found
revoked / expired401Key no longer usable
forbidden403Key not permitted on this project
insufficient_scope403Scope missing; details.requiredScope names it
not_found404Unknown resource, malformed id, or unmapped route
validation400Bad or missing field; details explains
conflict409If-Match version did not match
rate_limited429Per-tier rpm/rph exceeded; Retry-After header
quota_exceeded402Monthly quota exhausted; details.metric names it
internal500Bug — report it

Always branch on error.code, never on error.message (it's human-readable and may change).

Idempotency

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.

Optimistic concurrency

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.

Rate limits and quotas

  • Rate limits are per key, enforced in minute and hour windows. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. On 429, honour Retry-After.
  • Quotas are per user, enforced monthly by metric (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.

Self-describing endpoints

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.

Working with projects

GET /projects

Lists 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/:pid

Fetches a single project. Returns { id, name, slug, description, createdAt, updatedAt, onboardedAt }.

Working with fibers

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/fibers

List 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/:fid

Fetch a single fiber. data is returned parsed (not a JSON string).

POST /projects/:pid/fibers

Create 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/:fid

Update 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.
  • Body 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/:fid

Soft-deletes the fiber and removes its embeddings. Requires fibers:delete.

POST /projects/:pid/fibers/:fid/reembed

Explicitly rebuild the fiber's embedding. Useful when upstream embedding settings change. Returns 202 with a pending task.

Asset references inside fiber data

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…" } }

Working with threads

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/threads

Returns the full thread list. Each thread has { id, title, slug?, summary?, funnelStage?, components[], createdAt, updatedAt }.

GET /projects/:pid/threads/:tid

Fetches a single thread.

POST /projects/:pid/threads

Create 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/:tid

Merge or replace thread fields. op: "replace" discards the existing components array.

DELETE /projects/:pid/threads/:tid

Removes the thread. Requires threads:delete.

POST /projects/:pid/threads/reorder

Reorders 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.

Components on a thread

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

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/style

Returns the current style.

PATCH /projects/:pid/style

Deep-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-url

Scrapes 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.

Assets and media

Agents upload media in two steps — request a presigned URL, PUT the bytes, then finalize. Dedupe is by SHA-256.

POST /projects/:pid/assets/uploads

Ask 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/finalize

After 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/:aid

List (filter with ?kind=image|video|model|font|audio) or fetch one.

DELETE /projects/:pid/assets/:aid

Deletes the asset row. The R2 object is retained (safer — published builds may reference it).

POST /projects/:pid/assets/:aid/depth

Re-run depth-map generation. Requires assets:write and depth quota.

Search

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.

Publishing

GET /projects/:pid/publish

Publish status: { productId, publishedAt, version, publishedUrl, pageUrl, llmTxtUrl, videoSitemapUrl }.

POST /projects/:pid/publish

Kicks 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.

Tasks

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 /tasks

Recent tasks for the authenticated user. ?status=<status>&limit=<n> filter.

POST /tasks/:id/cancel

Signal cancellation. Running tasks best-effort stop at the next checkpoint.

Scope reference

Scopes are namespaced resource:action. Bundles:

  • agent:readonlyprojects: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.

Recipes

Describe-to-storefront

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"

Continuous sync from an external PIM

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 }'

Handling quota exhaustion gracefully

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

Debugging checklist

  • 401 unauthorized — the bearer header is missing, malformed, or the key is in the wrong mode (test vs. live). Call /me to confirm the key parses.
  • 403 insufficient_scopeerror.details.requiredScope tells you exactly which scope the key is missing. Issue a new key with that scope.
  • 403 forbidden — the key has the right scope but isn't allowed on this project. Check the key's projectIds allowlist.
  • 404 not_found on a valid-looking id — either the id format is wrong (Convex ids are 32 lowercase alphanumerics) or the resource belongs to a different user.
  • 409 conflict — you sent If-Match with a stale version. Re-fetch and retry.
  • 402 quota_exceeded — you're out of budget for the month. error.details.metric and resetsAt tell you which and when.
  • 429 rate_limited — back off per Retry-After. If you see this often, your agent is too chatty; batch where possible.

Next steps

  • Grab the skill file: curl https://api.oeave.com/agent/v1/skill
  • Browse the JSON catalog: curl https://api.oeave.com/agent/v1/docs
  • Read the companion feature page: Agent-First Editing
  • Or pair with agentic commerce on the read side: Agentic Commerce