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.