Personal Access Tokens (PATs)
PATs are long-lived, per-organization bearer tokens. Every MCP request carries one in the Authorization header:
Authorization: Bearer vbl_pat_live_<32 urlsafe chars>Token Format
| Component | Example | Meaning |
|---|---|---|
| Prefix | vbl_pat_live_ | Production. Registered with GitHub secret scanning. |
| Prefix | vbl_pat_test_ | Non-production environments. |
| Body | 32 URL-safe characters | 24 bytes of entropy from secrets.token_urlsafe(). |
At creation the full token is shown once. Only a SHA256 hash of the raw value is persisted — a stolen database row cannot be replayed as a token.
Lifecycle
Create
Dashboard → Settings → API & MCP → Create token. Choose a label and scopes. Up to ten active PATs per user per organization.
Rotate
Create a new token, swap your client config over, then revoke the old one. Tokens have no expiry — rotation is entirely on your schedule.
Revoke
Any row in the Settings list can be revoked. Rejection is immediate; in-flight requests that already passed auth complete normally.
Audit
Each token row shows when it was last used and the IP address of that call (last_used_ip), so a token used from somewhere you don't recognize is easy to spot. Use Edit scopes on any row to widen or narrow a token's access without minting a new one.
OAuth Connectors
Claude.ai, Claude Desktop connectors, and ChatGPT cannot send a static token. They connect through OAuth 2.1 instead: add https://mcp.vaybel.com/ as a custom connector, sign in to Vaybel, and approve one workspace and a scope set on the consent screen. The resulting access is org-pinned exactly like a PAT, shows up under Connected apps in Settings, and can be revoked there. Details in Connectors.
Where To Store Tokens
Use one canonical local secret: VAYBEL_PAT.
For Claude Code, prefer storing the token in Claude Code settings so it is available to both the Vaybel MCP server and the official Vaybel Skills plugin:
{
"env": {
"VAYBEL_PAT": "vbl_pat_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}Then reference it from MCP config instead of pasting the token into the config file:
{
"mcpServers": {
"vaybel": {
"type": "http",
"url": "https://mcp.vaybel.com/",
"headers": {
"Authorization": "Bearer ${VAYBEL_PAT}"
}
}
}
}Do Not Commit Tokens
Do not put raw vbl_pat_live_... values in project .mcp.json, example repos, screenshots, issue comments, or chat transcripts. Avoid putting long-lived Vaybel tokens in broad shell startup files like .zshrc unless you intentionally want every terminal process to inherit them.
Scopes
Every tool declares the scope(s) it requires. A PAT missing the scope gets a missing_scope tool error. The dashboard token creator selects all public scopes by default — uncheck only what you want to deny.
All scopes. The creator defaults to an All scopes switch that stores the literal ["*"] wildcard. Unlike an explicit scope list — which is fixed at mint time, so a scope added to the platform later isn't granted until you create a fresh token or edit the token's scopes — an all-scopes token grants every current scope and any scope added later, so it never needs recreating for new tools. Turn the switch off to pick an explicit subset.
Read scopes
| Scope | Tools |
|---|---|
design:read | workspace.open (also needs mockup:read), design.get_generation, design.list, design.get, design.get_history |
mockup:read | workspace.open (also needs design:read), mockup.get_generation, mockup.list, mockup.get, mockup.show |
brand_dna:read | brand_dna.get, brand_dna.list_audience_presets |
catalog:read | catalog.list_blanks, catalog.get_blank, catalog.compare_blanks |
credits:read | credits.check, credits.list_costs, credits.list_usage |
product_video:read | product_video.get_generation |
virtual_model:read | virtual_model.list, virtual_model.get_generation |
listing:read | listing.get, listing.list, listing.get_generation, listing.list_warehouses |
content:read | content.get, content.list |
social_post:read | social_post.list, social_post.get |
trend:read | trend.list, trend.get, trend.list_keywords, trend.get_keyword, trend.get_generation, trend.list_seasonal_events |
insight:read | insight.get_overview, insight.list_design_performance, insight.get_guidance |
integration:read | integration.list |
optimize:read | optimize.list_providers, optimize.list_provider_products, optimize.check_duplicate, optimize.get_generation |
drop:read | drop.list, drop.get, drop.related_trends |
Write scopes
| Scope | Tools |
|---|---|
brand_dna:write | brand_dna.set |
design:write | design.generate, design.edit, design.submit_feedback |
mockup:write | mockup.generate, mockup.update_selection, mockup.submit_feedback, mockup.retry |
product_video:write | product_video.generate |
virtual_model:write | virtual_model.generate |
listing:write | listing.create, listing.update, listing.regenerate_field, listing.publish, listing.delete, listing.sync_warehouses |
content:write | content.generate, content.delete |
social_post:write | social_post.generate, social_post.update, social_post.publish |
trend:write | trend.generate_concept, trend.record_outcome |
optimize:write | optimize.import_product, optimize.refresh_listing |
drop:write | drop.create, drop.update, drop.attach, drop.pin_trend |
PrintGen buyer scopes
Tokens for the PrintGen buyer surface carry a smaller, buyer-facing scope set:
| Scope | Tools |
|---|---|
design:write | design.generate, design.edit |
design:read | design.get_generation, design.get, design.list, design.browse (also needs mockup:read) |
mockup:read | design.browse (also needs design:read), mockup.get_generation, mockup.get, mockup.show |
mockup:write | mockup.generate |
catalog:read | shop.list_products, shop.get_product |
checkout:write | shop.create_checkout |
order:read | shop.get_checkout, shop.list_orders, shop.get_order |
Principle of Least Privilege
A monitoring agent that polls status doesn't need any :write scope — give it the :read set and nothing more. A trend-discovery agent that proposes drafts can have all reads plus design:write + mockup:write, but not listing:write or content:write. Build narrow tokens.
Plan gates
Most of the MCP surface is a paid capability. A small read-only set works on any plan, most tools require Starter or higher, and the Growth-plan feature areas are gated at Growth:
| Tier | Tools |
|---|---|
| Open (any plan) | ping, catalog.*, brand_dna.get, credits.check, drop.list, drop.get, drop.related_trends |
| Starter+ | brand_dna.set, brand_dna.list_audience_presets, credits.list_costs, credits.list_usage, design.*, mockup.*, virtual_model.*, listing.*, trend.*, integration.list, workspace.open, drop.create, drop.update, drop.attach, drop.pin_trend |
| Growth | content.*, insight.*, optimize.*, product_video.*, social_post.* |
Gated calls fail with a clear plan_required: ... error rather than returning empty data. The error names no plan; each tool's tier is shown as a Plan chip in the Tool Reference.
Rate Limits
Rate limits apply per-PAT, per-tool, on a fixed 60-second window. Hitting one raises a rate_limited error carrying a retry after Ns hint.
| Tool | Limit |
|---|---|
brand_dna.set | 10 / min |
design.generate | 10 / min |
design.edit | 10 / min |
design.submit_feedback | 60 / min |
mockup.generate | 20 / min |
mockup.update_selection | 60 / min |
mockup.submit_feedback | 60 / min |
mockup.retry | 20 / min |
product_video.generate | 10 / min |
virtual_model.generate | 10 / min |
listing.create | 10 / min |
listing.update | 20 / min |
listing.regenerate_field | 10 / min |
listing.publish | 5 / min |
listing.delete | 20 / min |
listing.list_warehouses | 10 / min |
listing.sync_warehouses | 10 / min |
content.generate | 5 / min |
content.delete | 20 / min |
social_post.generate | 10 / min |
social_post.publish | 10 / min |
trend.generate_concept | 10 / min |
trend.record_outcome | 60 / min |
optimize.list_provider_products | 30 / min |
optimize.import_product | 10 / min |
optimize.refresh_listing | 20 / min |
| All other public tools | No explicit MCP-layer limit (platform-level limits still apply) |
Credit balance provides the finer gating for billable tools — you'll hit insufficient_credits before a rate limit on a real workload.
Credit Charging
Most tools are free. A handful charge from the same credit balance your dashboard uses — no separate developer meter.
| Tool | Charged for | Credits | Charged |
|---|---|---|---|
design.generate | Each design | 5 | When it completes |
design.edit | Each edit | 3 | When it completes |
mockup.generate | Each product photo at Pro quality, or in an environment | 2 | When it completes |
mockup.generate | Each print or fabric close-up | 2 | When it completes |
mockup.generate | Each model photo | 2 | When it completes |
product_video.generate | Each video | 20 | When it completes |
virtual_model.generate | Each model generated | 1 | When the call is accepted |
listing.create | Each draft listing | 2 | When the call is accepted |
content.generate | A video | 40 | When it completes |
content.generate | A slideshow | 12 | When it completes |
content.generate | A carousel | 4 | When it completes |
content.generate | A single image | 1 | When it completes |
trend.generate_concept | Each new concept | 2 | When the call is accepted |
Work that is charged when it completes costs nothing if it fails. Product photos from mockup.generate at quality="standard" are free, and dry_run=true returns the exact quote before anything is charged. credits.list_costs returns the same prices.
Three pre-flight gates prevent surprises:
- Out-of-credit calls fail fast — billable tools run a balance check before dispatching any background work.
- Billable tools accept an
idempotency_key; sending the same key on a retry returns the cached result, so a flaky-network loop doesn't double-bill (or double-publish). trend.generate_conceptshort-circuits to free when the concept is already cached.
Read tools and status polls are always free.
Errors You Might See
Tool errors arrive as MCP tool results (not HTTP status codes — only authentication failures are HTTP 401). Machine-readable errors are prefixed with a stable code:
| Error | Cause | Fix |
|---|---|---|
HTTP 401 missing bearer token | No Authorization header | Re-check your client config; restart after changes |
HTTP 401 unknown token | Revoked or typo'd PAT | Revoke from Settings, create a fresh one |
missing_scope: ... | PAT lacks the scope the tool needs | Re-create the PAT with the missing scope |
rate_limited: ... retry after Ns | Per-tool per-PAT limit hit | Wait the indicated seconds; batch where possible |
insufficient_credits: requires N, available M | Balance below cost | Top up credits, or use quality="standard" for mockups |
plan_required: ... | Tool needs a higher plan (trends → Starter+, insights → Growth) | Upgrade the org's plan |
not_found: ... | Unknown id — or a UUID belonging to another org | Each PAT is pinned to one org; re-check the id |
invalid_argument: ... | An argument failed validation | The message names the field; fix it and call again |
forbidden: ... | The tool is not open to this kind of token | Call only the tools listed in the Tool Reference |
upstream_failed: ... | A sales channel or publish target rejected the operation | Check the connection with integration.list, then retry |
budget_exceeded: ... | Session budget cap reached (internal session tokens) | Raise the session budget |
Errors without a code prefix are plain validation messages (e.g. prompt is required).