The /v1 API lets a workspace create, list, retarget, pause, activate and delete hosted QR codes, and read its plan usage. Every code gets a public scan URL at the domain root: https://vlcard.me/<short_code>. Scans get a 302 to the code's current destination, so you can change where a printed QR goes without reprinting it.
Call the API from your server. Browser calls from other origins are blocked by CORS, and an API key in front-end code is a leaked key.
Requests and responses are JSON (Content-Type: application/json). Timestamps are RFC 3339 in UTC. Codes created through the API always have origin set to api.
Destination rules (checked on create and update, same rules as the dashboard):
http or https URL with a host, at most 2048 bytes after trimming surrounding whitespace.
No spaces or control characters inside the URL, no user@ userinfo, no IP-literal hosts such as [::1], and a port of 1 to 5 digits if present.
Percent escapes in the path and fragment must be valid and must not encode NUL, CR or LF. The query string is passed through as is.
A destination that breaks any rule is rejected with 400 invalid_destination.
Base URL: https://vlcard.me/v1
Authentication
A workspace API key, sent as X-API-Key: vlq_…. Managers of a workspace whose plan includes API access mint keys in the dashboard under API. The full key is shown once; only a hash is stored, so if you lose it, revoke it and mint a new one. A key acts on its own workspace only.
Rate limits
There are two budgets, chosen after the key is looked up.
Valid key: limited per key to your plan's requests per minute (Business 120, Enterprise 600, Partner 300, Scale 1,200, Platform 3,000). The window is a fixed minute that starts with the first request. Over the limit you get 429 rate_limited with no rate-limit headers; wait for the minute to pass.
Missing, unknown, revoked or no-access key: 60 requests per minute per client IP, shared by every protected /v1 route. These responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). The 61st request gets 429 rate_limited with Retry-After in seconds.
GET /v1/health is never rate limited. Scans of https://vlcard.me/<short_code> never count against these limits and are never answered with 429.
Errors
Errors are JSON with one field, error, holding a stable snake_case code: {"error": "not_found"}. Branch on the code, not on the HTTP status alone. Unmatched paths under /v1 return 404 not_found.
Error codes
Code
Status
Meaning
missing_api_key
401
No X-API-Key header.
invalid_api_key
401
The key is unknown, mistyped or revoked.
api_access_required
403
The workspace's plan doesn't include API access.
rate_limited
429
The per-key or no-key IP budget is spent.
not_found
404
No such code in this workspace, a malformed short code, or an unknown /v1 path.
invalid_json
400
The body isn't a JSON object with the documented field types.
invalid_limit
400
limit isn't an integer of 1 or more.
invalid_offset
400
offset isn't an integer of 0 or more.
invalid_expiry
400
expires_at isn't an RFC 3339 timestamp.
invalid_name
400
name is neither a string nor null.
invalid_destination
400
The destination is missing or breaks the destination rules.
expiry_must_be_future
400
expires_at isn't in the future.
expiry_exceeds_plan_limit
400
expires_at is further out than the plan allows.
ads_not_available
400
has_ad is true on a plan without an ad-supported pool.
invalid_status
400
Reserved; not returned by the current endpoints.
active_limit_reached
403
The clean active pool is full.
ad_limit_reached
403
The ad-supported active pool is full.
library_limit_reached
403
The workspace holds as many codes as the plan allows.
redirect_quota_reached
403
This month's redirect units are used up.
code_expired
409
The code is past expires_at and can't be activated.
internal_error
500
Unexpected server error.
Codes
Hosted QR codes owned by the key's workspace.
GET/v1/codes
List codes
Returns the workspace's codes, newest first. The list includes codes created in the dashboard, not only API-created ones. total is the count of all codes in the workspace, so page with offset until you have seen total rows.
GET /v1/codes parameters
Field
Type
Description
limitquery
integer
Page size, 1 or more. Values above 100 are treated as 100.
Creates an active code with a new six-character short_code. The code counts against the plan's active pool (clean, or ad-supported when has_ad is true), library size and monthly redirect quota. Its scan URL is https://vlcard.me/<short_code>.
POST /v1/codes request body
Field
Type
Description
destinationrequired
string (uri)
http or https URL that follows the destination rules. Surrounding whitespace is trimmed. maxLength is in bytes.
name
string | null
Optional label. Trimmed; empty means no name.
expires_at
string (date-time) | null
Optional RFC 3339 timestamp in the future, within the plan's maximum expiry. null or an empty string means no expiry.
has_ad
boolean
Partner plans (Partner, Scale, Platform) only. Puts the code in the ad-supported pool, whose scans cost 0.25 redirect units. On other plans true returns 400 ads_not_available. Default false.
A code deleted from the dashboard is not returned by the API (list or get); it answers 404 like a code that never existed.
GET /v1/codes/{code} parameters
Field
Type
Description
codepathrequired
string
The code's six-character short_code (not its id). Case-sensitive. Anything that isn't six letters or digits, or belongs to another workspace, returns 404 not_found.
Sets a new destination and optionally changes the name. Scans follow the new destination straight away. The short code, status and expiry don't change.
PATCH /v1/codes/{code} parameters
Field
Type
Description
codepathrequired
string
The code's six-character short_code (not its id). Case-sensitive. Anything that isn't six letters or digits, or belongs to another workspace, returns 404 not_found.
PATCH /v1/codes/{code} request body
Field
Type
Description
destinationrequired
string (uri)
The new destination. Required on every update, even if only the name changes.
name
string | null
Leave the field out to keep the current name. Send a string to set it, or null or "" to remove it.
Permanently deletes the code and its scan analytics. The scan URL stops redirecting at once. To stop scans but keep the code, pause it instead.
DELETE /v1/codes/{code} parameters
Field
Type
Description
codepathrequired
string
The code's six-character short_code (not its id). Case-sensitive. Anything that isn't six letters or digits, or belongs to another workspace, returns 404 not_found.
Sets status to paused. Scans show an "unavailable" page instead of redirecting, and the code stops counting against the active pool. It stays in the library. No request body. Pausing an already paused code succeeds.
POST /v1/codes/{code}/pause parameters
Field
Type
Description
codepathrequired
string
The code's six-character short_code (not its id). Case-sensitive. Anything that isn't six letters or digits, or belongs to another workspace, returns 404 not_found.
Request
curl -s -X POST https://vlcard.me/v1/codes/aB3xY9/pause \
-H "X-API-Key: $VLQ_API_KEY"
Sets status to active so scans redirect again. Re-checks the plan's active pool for a paused code. A code past its expires_at can't be activated; create a new code instead. No request body.
POST /v1/codes/{code}/activate parameters
Field
Type
Description
codepathrequired
string
The code's six-character short_code (not its id). Case-sensitive. Anything that isn't six letters or digits, or belongs to another workspace, returns 404 not_found.
Request
curl -s -X POST https://vlcard.me/v1/codes/aB3xY9/activate \
-H "X-API-Key: $VLQ_API_KEY"
A snapshot of the workspace's plan and how much of it is used. A null limit means unlimited. Redirect units reset each calendar month (Africa/Johannesburg); a clean scan costs 1 unit and an ad-supported scan 0.25. When over_quota is true, existing codes keep redirecting but creates fail with redirect_quota_reached.