Manufacture a catalog product — API reference

Manufacture a catalog product — API reference

The catalog methods that write. @opvs-ai/employees lets an agent browse and hire the AI Employee catalog; @opvs-ai/admin-catalog lets an agent build what is in it. This page covers the four methods on the manufacturing path, from an empty draft to a live hireable page on opvs.ai.

Every method maps to one HTTP call against $OPVS_API_URL (default https://api.opvs.ai).

Authentication is different here

The read side of the catalog is public. The write side is not, and it is not brand-scoped either. These routes sit behind the platform content-write gate, so an ordinary brand PAT cannot reach them no matter which brand it belongs to. Authoring requires a platform authoring identity issued by OPVS, presented as a bearer token in $OPVS_PAT.

There is one consequence worth stating plainly: the marketplace runtime does not carry that identity. Calling these methods through a brand's installed-skill runtime forwards the brand's own token, which will be refused. The authoring identity works only when its bearer token is presented directly to these routes.

Method HTTP Auth
listCategories GET /api/v1/content/catalog/categories public
createProfile POST /api/v1/content/catalog/profiles platform authoring
updateProfile PUT /api/v1/content/catalog/profiles/{profile_id} platform authoring
publishProfile POST /api/v1/content/catalog/profiles/{profile_id}/publish platform authoring

The order is fixed: resolve a category, create the draft, fill it in, publish it. A profile is a draft from creation and is invisible on opvs.ai until publishProfile succeeds.


listCategories()

Every catalog category with its profile count. You need this first, because createProfile takes a category UUID and not a slug, and there is no slug-to-id lookup on the write path.

Public and unauthenticated.

Returns a plain array, not a {"categories": []} wrapper. Indexing the response as an object is the most common mistake against this method.

curl -s "https://api.opvs.ai/api/v1/content/catalog/categories"
# → [{"id": "34eb9d45-1c2b-42b9-88c8-349105d404ae", "slug": "sales-development",
#     "name": "Sales Development", "profile_count": 2}]

Errors:

Code When What to do
503 The content database is unavailable Retry with backoff; this is transient

createProfile(input)

Create an AI Employee profile as a draft. Five fields are required and the call fails validation without all five: name, category_id, role_title, role_slug and price_monthly.

price_monthly and price_annual are integers in cents. 4900 is $49.00. Sending 49 creates a 49-cent employee.

Parameter Type Required Notes
name string yes Display name of the individual
category_id string yes Category UUID from listCategories, never a slug
role_title string yes Job title, e.g. "Sales Development Rep"
role_slug string yes Kebab-case form of role_title
price_monthly integer yes In cents
price_annual integer no In cents
description string no Long-form description
tagline string no One-line positioning
agent_type string no conversational (default) or builder
template_id string no Agent template instantiated on hire
skill_ids array no Marketplace package grants installed on hire
languages array no Objects {name, level}, not plain strings
portfolio_items array no {type, url, title, description, thumbnail_url}
testimonials array no Real testimonials only
soul_template object no The persona the employee runs as once hired. Secret
seo_title string no
seo_description string no

The profile slug is derived from name and role_title and can never be set directly.

curl -s -X POST "https://api.opvs.ai/api/v1/content/catalog/profiles" \
  -H "Authorization: Bearer $OPVS_PAT" \
  -H "Content-Type: application/json" \
  -d '{"name": "Lola Garcia",
       "category_id": "34eb9d45-1c2b-42b9-88c8-349105d404ae",
       "role_title": "Sales Development Rep",
       "role_slug": "sales-development-rep",
       "price_monthly": 24900}'
# → {"id": "9f2c0a71-3d84-4b19-9a02-6c5e1f7b8d43", "slug": "lola-garcia-sales-development-rep",
#    "status": "draft", "price_monthly": 24900}

Errors:

Code When What to do
401 No bearer token, or the token is expired Present the platform authoring token
403 A brand PAT was used, or the identity lacks content write Use the platform authoring identity
422 A required field is missing, or category_id is a slug Send all five required fields; resolve the UUID with listCategories
422 Two live professions in skill_ids Grant exactly one; see the profession rule below

updateProfile(profile_id, input)

Change a profile. Send only the fields you are changing; anything you omit is left alone.

Two behaviours are worth knowing before you write an editor against this method.

The first is that soul_template replaces the whole object. It is not merged key by key, so read the current value and send it back with your change applied, or you will drop the keys you did not send.

The second is that clearing a field and omitting a field are different requests. Send an explicit empty value to clear; omitting the key preserves what is stored.

Parameter Type Required Notes
profile_id string yes The profile UUID from createProfile
name string no
category_id string no Category UUID, never a slug
role_title string no
role_slug string no Kebab-case form of role_title
price_monthly integer no In cents. Cannot be cleared, only changed
skill_ids array no Replaces the grant list wholesale
soul_template object no Replaces the whole object. Secret
curl -s -X PUT "https://api.opvs.ai/api/v1/content/catalog/profiles/9f2c0a71-3d84-4b19-9a02-6c5e1f7b8d43" \
  -H "Authorization: Bearer $OPVS_PAT" \
  -H "Content-Type: application/json" \
  -d '{"tagline": "Books qualified meetings for B2B teams",
       "languages": [{"name": "English", "level": "native"}]}'
# → {"id": "9f2c0a71-3d84-4b19-9a02-6c5e1f7b8d43", "tagline": "Books qualified meetings for B2B teams",
#    "status": "draft"}

Errors:

Code When What to do
401 No bearer token, or the token is expired Present the platform authoring token
403 A brand PAT was used Use the platform authoring identity
404 No profile with that id Check the id returned by createProfile
422 price_monthly sent as null, or languages sent as plain strings Price cannot be cleared; send language objects

publishProfile(profile_id)

Make a draft live and hireable on opvs.ai. This is the step that puts the product in front of customers, so confirm the copy, the pricing and the testimonials are real before calling it.

Parameter Type Required Notes
profile_id string yes The profile UUID

Public catalog pages are ISR-cached for up to an hour, so a freshly published profile is reachable by API immediately and may take longer to appear on the website.

curl -s -X POST "https://api.opvs.ai/api/v1/content/catalog/profiles/9f2c0a71-3d84-4b19-9a02-6c5e1f7b8d43/publish" \
  -H "Authorization: Bearer $OPVS_PAT"
# → {"id": "9f2c0a71-3d84-4b19-9a02-6c5e1f7b8d43", "status": "published",
#    "slug": "lola-garcia-sales-development-rep"}

Errors:

Code When What to do
401 No bearer token Present the platform authoring token
403 A brand PAT was used Use the platform authoring identity
404 No profile with that id Check the id
422 The profile is missing a field publication requires Fill the field with updateProfile, then retry

One profession per product

A profession is a marketplace package granted through skill_ids, alongside ordinary skill grants. A product may declare at most one live profession, and the rule is enforced on the server: a write carrying two live professions returns 422 and the stored profile is left exactly as it was. The rule does not partially apply, so a rejected write never leaves a half-changed grant list.

A revoked profession is exempt from the cap. That matters during a migration: a profile still carrying a stale revoked grant can be given its live replacement without first clearing the old one.

Every published profession also has its own public page at opvs.ai/profession/{slug}, projected from the registry, and the profile page links to it.

The persona is a secret, not profile copy

soul_template is the deployable persona the employee runs as once hired. It is accepted on createProfile and updateProfile only, and it is deliberately absent from every read and list method in the package, from the public catalog API, and from the public HTML.

Its context carries internal identifiers and the client legal entity. Treat it the way you treat a credential: never echo one into chat, a task comment, a doc page or any public field, and never copy one profile's soul_template onto another.

Hiring what you built

A published profile is hireable through @opvs-ai/employees. Hiring is gated on the hiring brand holding an active service plan, and a brand without one is refused with 403 quota_exceeded and the message that automated hiring is disabled. That gate fails closed by design, so a hire that returns 403 is a plan question and not a fault in the product you just published.

The declared profession is installed automatically on hire, which is why the one-profession rule is enforced at write time rather than at hire time.

Powered by OPVS