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 |
|---|---|
read | GET endpoints (list, detail, QR, analytics, markdown versions, jobs). |
write | Create, update, upload, redeploy, restore, and optimize endpoints. |
delete | Soft-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/me | read | Account profile, storage quota, and feature flags (including features.artifacts_enabled). |
GET | /api/v1/openapi.yaml | — | Public 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/resources | read | Paginated owned resources. Query: type, q, sort, label, archived, pinned, cursor, offset, limit. Returns a weak ETag and counts. |
GET | /api/v1/resources/{slug} | read | Full editable detail with an ETag for later If-Match. |
POST | /api/v1/resources | write | JSON create for link, markdown, collection, notion, or bio (type discriminator). |
PATCH | /api/v1/resources/{slug} | write | Partial update; optional If-Match → 412 etag_mismatch; slug conflict → 409 slug_taken. |
DELETE | /api/v1/resources/{slug} | delete | Soft-delete (deleted_at). |
GET | /api/v1/resources/{slug}/qr | read | QR image for the public URL (?format=png|svg, ?size, ?src=qr). |
POST | /api/v1/uploads | write | multipart file upload; sniffed MIME creates a file or image resource. |
POST | /api/v1/artifacts | write | multipart HTML artifact publish (metadata JSON + ZIP or mapped files). |
GET | /api/v1/resources/{slug}/artifact/files | read | List the current deployment's canonical files and text-editability metadata. |
GET | /api/v1/resources/{slug}/artifact/source | read | Read one verified UTF-8 source file (up to 1 MiB) in a no-store JSON response. |
POST | /api/v1/resources/{slug}/artifact/deployments | write | Publish a full replacement or sparse immutable artifact deployment; optional If-Match. |
Markdown versions
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /api/v1/resources/{slug}/markdown/versions | read | Markdown version history (newest first; retained to 50). |
GET | /api/v1/resources/{slug}/markdown/versions/{versionID} | read | One historical markdown snapshot. |
POST | /api/v1/resources/{slug}/markdown/versions/{versionID}/restore | write | Restore 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-original | write | Queue original-file optimization (202). Eligible: large JPEG or HEIC/HEIF. |
GET | /api/v1/jobs | read | List owned background jobs (?status, ?type, ?limit, ?cursor, ?offset). |
GET | /api/v1/jobs/{id} | read | One job's status, result, error, attempts, and suggested next poll delay. |
GET | /api/v1/analytics/resources/{slug} | read | Hits, 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 |
|---|---|---|
link | target_url | max_views |
markdown | raw_markdown or body_markdown | max_views |
collection | — | — |
notion | source_url or fetch_url | root_page_id |
bio | — | display_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_key | 401 | Missing, malformed, unknown, or revoked bearer key. |
insufficient_scope | 403 | The key lacks read, write, or delete. |
not_found | 404 | Resource, job, or version not found — or not owned, deleted, or expired. |
slug_taken | 409 | Slug already in use. |
job_already_active | 409 | An equivalent background job is already running. |
etag_mismatch | 412 | If-Match did not match the current state; re-read and retry. |
payload_too_large | 413 | Body or upload exceeded a size limit. |
rate_limited | 429 | Too many requests; wait for Retry-After. |
quota_exceeded | 400 | Storage quota exceeded. |
validation_failed | 400 | Invalid field or ineligible optimize request; check field. |
feature_disabled | 400 | The user's feature flag for this action is off. |
artifacts_disabled | 503 | HTML Artifact creation or serving is disabled. |
artifact_archive_invalid | 400 | ZIP failed validation (unsafe, encrypted, or expansion-ratio). |
artifact_path_invalid | 400 | A logical path is invalid, reserved, duplicated, or colliding. |
artifact_index_missing | 400 | No eligible non-empty HTML entry, or root index.html is empty. |
artifact_too_large | 413 | An 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.