vee.kr

REST

API v1

Complete bearer-authenticated JSON API for links, markdown, files, images, collections, Notion mirrors, bio pages, and HTML artifacts. OpenAPI is the machine-readable companion.

Authentication

Create a key in the dashboard, then send it on every request except the public OpenAPI download. The plaintext is shown once at creation; the server stores sha256(plaintext).

Authorization: Bearer bdma_<key>
Scope Allows
readGET endpoints (list, detail, QR, analytics, markdown versions, jobs).
writeCreate, update, upload, redeploy, restore, and optimize endpoints.
deleteSoft-delete a resource you own.
  • invalid_api_key — Missing, malformed, unknown, or revoked bearer key.
  • user_disabled — The owning user account is disabled.
  • insufficient_scope — The key lacks read, write, or delete.

Conventions

  • Errors are JSON {error, error_code, field?, details?}. error is human-readable; error_code is the stable machine token.
  • Rate limiting is per identity (API key id, else IP). Responses may carry X-RateLimit-* headers; exhaustion returns 429, Retry-After, and rate_limited.
  • Ordinary JSON mutations are capped at 1 MiB. Uploads follow MAX_UPLOAD_MB; artifacts have separate ZIP/file/count ceilings. MCP POST /mcp JSON allows up to 12 MiB for base64 payloads.
  • GET /resources returns a weak list ETag. GET /resources/{slug} returns an editable-state ETag; PATCH accepts If-Match and returns 412 etag_mismatch when stale.
  • Deleted, disabled-owner, and expired resources return 404 not_found (no existence leak). Past max_views uses the configured expired state.
  • Every endpoint is scoped to the authenticated user's own resources; another owner's slug returns 404.
  • Owner requests never increment hit_count or create page_views rows.
  • List endpoints are offset-paginated. When next_cursor is present, pass it back as ?cursor=; cursor takes precedence over offset.

Quickstart

Confirm the key, then create a short link:

curl -sS \
  -H "Authorization: Bearer $REST_API_KEY" \
  https://<your-host>/api/v1/me
curl -sS \
  -H "Authorization: Bearer $REST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"link","target_url":"https://example.com","title":"Example"}' \
  https://<your-host>/api/v1/resources

Endpoints

Every registered /api/v1 route is listed below with its required scope. The OpenAPI download mirrors the same paths for codegen and exact JSON Schema.

Meta

Method Path Scope Description
GET/api/v1/mereadAccount profile, storage quota, and feature flags (including features.artifacts_enabled).
GET/api/v1/openapi.yamlPublic OpenAPI 3.1 document (IP rate-limited).

Remote MCP is OAuth-only and documented separately: MCP.

Resources and uploads

Method Path Scope Description
GET/api/v1/resourcesreadPaginated owned resources. Query: type, q, sort, label, archived, pinned, cursor, offset, limit. Returns a weak ETag and counts.
GET/api/v1/resources/{slug}readFull editable detail with an ETag for later If-Match.
POST/api/v1/resourceswriteJSON create for link, markdown, collection, notion, or bio (type discriminator).
PATCH/api/v1/resources/{slug}writePartial update; optional If-Match → 412 etag_mismatch; slug conflict → 409 slug_taken.
DELETE/api/v1/resources/{slug}deleteSoft-delete (deleted_at).
GET/api/v1/resources/{slug}/qrreadQR image for the public URL (?format=png|svg, ?size, ?src=qr).
POST/api/v1/uploadswritemultipart file upload; sniffed MIME creates a file or image resource.
POST/api/v1/artifactswritemultipart HTML artifact publish (metadata JSON + ZIP or mapped files).
GET/api/v1/resources/{slug}/artifact/filesreadList the current deployment's canonical files and text-editability metadata.
GET/api/v1/resources/{slug}/artifact/sourcereadRead one verified UTF-8 source file (up to 1 MiB) in a no-store JSON response.
POST/api/v1/resources/{slug}/artifact/deploymentswritePublish a full replacement or sparse immutable artifact deployment; optional If-Match.

Markdown versions

Method Path Scope Description
GET/api/v1/resources/{slug}/markdown/versionsreadMarkdown version history (newest first; retained to 50).
GET/api/v1/resources/{slug}/markdown/versions/{versionID}readOne historical markdown snapshot.
POST/api/v1/resources/{slug}/markdown/versions/{versionID}/restorewriteRestore a version atomically; optional If-Match; returns a new ETag.

Jobs, optimize, and analytics

Method Path Scope Description
POST/api/v1/resources/{slug}/file/optimize-originalwriteQueue original-file optimization (202). Eligible: large JPEG or HEIC/HEIF.
GET/api/v1/jobsreadList owned background jobs (?status, ?type, ?limit, ?cursor, ?offset).
GET/api/v1/jobs/{id}readOne job's status, result, error, attempts, and suggested next poll delay.
GET/api/v1/analytics/resources/{slug}readHits, visitors, daily counts, ranked buckets, optional bio_clicks.

Creating resources

POST /api/v1/resources accepts a type-discriminated JSON body. Common optional fields on all creates: slug (auto 8-char if omitted), title, description, visibility, password, expires_at, labels.

type Required type fields Optional type fields
linktarget_urlmax_views
markdownraw_markdown or body_markdownmax_views
collection
notionsource_url or fetch_urlroot_page_id
biodisplay_name, headline, bio, theme

Successful creates return 201 with the type-specific ResourceDetail used by GET /resources/{slug}.

  • POST /api/v1/uploads uses multipart field file plus the common create fields. max_views is accepted only for non-image files.
  • POST /api/v1/artifacts uses multipart metadata (JSON) plus either archive (ZIP) or declared file parts. max_views is rejected for artifacts.
  • Collection item editing and bio page/block trees are cookie-API only. Bearer create with type=bio starts one empty root page.

HTML artifacts

Artifacts upload a static site. The share URL stays https://<host>/<slug>; after access checks Bidama hands the browser to https://<origin_label>.ar.phi.kr. metadata is a JSON object in the multipart field of the same name:

{
  "slug": "optional-slug",
  "title": "Optional title",
  "description": "Optional description",
  "visibility": "public",
  "password": "optional password",
  "expires_at": "2026-08-01T00:00:00Z",
  "labels": ["demo"]
}
  • ZIP mode: one file part named archive; omit metadata.files and other file parts.
  • Direct mode: metadata.files is [{part, path}, …] and each declared part appears exactly once as a multipart file. ZIP and direct modes cannot be mixed.
  • Entry point: non-empty root index.html if present, else the first non-empty .html/.htm file. Paths are NFC relative POSIX paths with traversal and reserved-prefix rejection.
  • Default ceilings: 50 MiB ZIP, 50 MiB per file, 100 MiB expanded, 500 files, 200:1 expansion, 512 KiB manifest. BIDAMA_ARTIFACT_MAX_* may lower them.
  • Redeploy metadata accepts only the optional files mapping. It creates a new origin, moves the current pointer, and never reuses the previous origin_label.

Errors

Failed requests return JSON with a human message and a stable code:

{
  "error": "Slug already in use.",
  "error_code": "slug_taken",
  "field": "slug"
}
error_code HTTP Description
invalid_api_key401Missing, malformed, unknown, or revoked bearer key.
insufficient_scope403The key lacks read, write, or delete.
not_found404Resource, job, or version not found — or not owned, deleted, or expired.
slug_taken409Slug already in use.
job_already_active409An equivalent background job is already running.
etag_mismatch412If-Match did not match the current state; re-read and retry.
payload_too_large413Body or upload exceeded a size limit.
rate_limited429Too many requests; wait for Retry-After.
quota_exceeded400Storage quota exceeded.
validation_failed400Invalid field or ineligible optimize request; check field.
feature_disabled400The user's feature flag for this action is off.
artifacts_disabled503HTML Artifact creation or serving is disabled.
artifact_archive_invalid400ZIP failed validation (unsafe, encrypted, or expansion-ratio).
artifact_path_invalid400A logical path is invalid, reserved, duplicated, or colliding.
artifact_index_missing400No eligible non-empty HTML entry, or root index.html is empty.
artifact_too_large413An artifact byte, file-count, or manifest limit was exceeded.

Useful conventions

  • List pages may include next_cursor — pass it back as ?cursor=.
  • Owner reads never increment public hit counts.
  • Deleted, expired, or foreign resources look like not_found.
  • MCP is OAuth-only at /mcp; API keys are REST-only.
  • Accepted optimize jobs return 202 with job_id, poll_url, and retry_after_seconds.

Also see: MCP · OpenAPI