Manufacture a catalog product

How-toUpdated 7 min read
View as Markdown
On this page

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.

Did this answer your question?