vee.kr

REST

REST API v1

A JSON API, authenticated with bearer API keys, for links, Markdown pages, files, images, collections, Notion mirrors, bio pages, and HTML artifacts.

Authentication

Create an API key on the API keys page in the dashboard. Bidama shows the full key once, right after you create it, and only stores a hash, so copy it somewhere safe. Send the key as a bearer token on every request. The only route that works without a key is the OpenAPI download.

Authorization: Bearer bdma_<key>

Each key carries one or more scopes. Scopes don't include each other, so a key with only write can't call GET routes. Give a key every scope it needs.

Scope What it allows
readEvery GET route, plus the two link simulation routes, which never change anything.
writeCreating, updating, uploading, redeploying, restoring, and queueing jobs.
deleteDeleting resources, collection items, bio media, and form submissions, and revoking upload requests.

If authentication fails you get one of these codes:

  • invalid_api_key (401): the key is missing, malformed, unknown, or revoked.
  • user_disabled (403): the account that owns the key has been disabled.
  • insufficient_scope (403): the key is valid but doesn't have the scope this route needs.

Quickstart

Check that your key works, then create a short link:

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

The response is the new resource, including its slug and public url.

How the API behaves

  • Your data only. Every route works on resources you own. A slug that doesn't exist, or a resource you deleted, returns 404 not_found. A resource that belongs to another account returns 403 forbidden on the resource routes and 404 elsewhere. Your own expired resources stay readable and editable through the API.
  • Errors. Failures return JSON with a readable error message and a stable error_code. Validation errors usually name the field as well. Match on error_code, not the message.
  • Rate limits. Every request counts against a budget for your IP address, and requests with a valid key also count against a budget for that key. Both default to 100 requests a minute, so several keys used from one machine share the IP budget. Creating links through POST /api/v1/resources has an extra limit of 60 an hour by default. Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. Going over returns 429 rate_limited with a Retry-After header.
  • Body size. Plain JSON requests can be up to 1 MiB. Uploads follow the server's upload limit (50 MiB unless the operator changed it), and HTML artifacts have their own limits (see below).
  • Safe edits. Reads and writes of a single resource return an X-Resource-ETag header. Send that value back unchanged as X-Resource-If-Match when you update the resource. If someone changed it in the meantime you get 412 etag_mismatch instead of silently overwriting their edit. Collection item routes use X-Collection-ETag and X-Collection-If-Match in the same way. The standard If-Match header still works, but new clients should use the X- headers, because proxies can rewrite a normal ETag.
  • Pagination. GET /resources and GET /jobs return next_cursor when there are more results; pass it back as ?cursor=. The submission lists take limit and offset and return a total. Upload requests and webhooks come back as a single list.
  • Analytics. Your own API calls never count as views of your resources.

Endpoints

This is every route under /api/v1, with the scope it needs. The OpenAPI document has the same routes with full request and response schemas, which is handy for generating a client.

Account

Method Path Scope Description
GET/api/v1/mereadYour profile, storage usage and quota, which resource types you may create, and server features such as features.artifacts_enabled.
GET/api/v1/openapi.yamlnoneThe OpenAPI 3.1 document. Public, and rate limited by IP address.

AI agents can use the same features through the MCP server, which signs in with OAuth instead of API keys.

Resources

Method Path Scope Description
GET/api/v1/resourcesreadList your resources. Filter with type, q, label, archived, pinned, and upload_request; order with sort; page with limit, offset, or cursor.
GET/api/v1/resources/{slug}readGet everything you can edit on one resource, plus its X-Resource-ETag.
POST/api/v1/resourceswriteCreate a link, Markdown page, collection, Notion mirror, or bio page from JSON.
POST/api/v1/resources/bulk-createwriteCreate up to 200 links in one call. Each item succeeds or fails on its own. Requires an Idempotency-Key header.
POST/api/v1/resources/importwriteImport links from a Bitly or Linktree CSV export. Requires an Idempotency-Key header.
PATCH/api/v1/resources/{slug}writeChange only the fields you send and get back the new slug and url. Taking a slug that is already in use returns 409 slug_taken. Visibility can't be changed after creation.
DELETE/api/v1/resources/{slug}deleteDelete a resource. Its public URL stops working right away.
GET/api/v1/resources/{slug}/qrreadA QR code for the public URL. format is png (the default) or svg; size is in pixels, 512 by default, between 128 and 2048.
GET/api/v1/resources/{id}/previewreadA small preview image, as linked from the list response. It never counts as a public view.
POST/api/v1/links/simulatereadTry out redirect rules and A/B variants for a link you haven't saved yet. Nothing is recorded.
POST/api/v1/resources/{slug}/link/simulatereadThe same dry run for a saved link. Fields you leave out keep their saved values.
POST/api/v1/uploadswriteUpload a file. Bidama checks the actual content against a list of allowed types and creates an image or a file resource.

HTML artifacts

Method Path Scope Description
POST/api/v1/artifactswritePublish a static site from a ZIP file or a set of individual files.
POST/api/v1/resources/{slug}/artifact/deploymentswritePublish a new version, either a full replacement or a patch that only uploads the files that changed.
GET/api/v1/resources/{slug}/artifact/filesreadList the files in the current version and whether each one can be edited as text.
GET/api/v1/resources/{slug}/artifact/sourcereadRead one text file (up to 1 MiB) from the current version. Pass its path in the query string.

Collections

Method Path Scope Description
POST/api/v1/resources/{slug}/collection/itemswriteAdd a copy of one of your file or image resources to the collection.
PATCH/api/v1/resources/{slug}/collectionwriteReorder items and change their display names and captions in one call.
DELETE/api/v1/resources/{slug}/collection/items/{itemID}deleteRemove one item from the collection.

Bio pages

Method Path Scope Description
PATCH/api/v1/resources/{slug}/biowriteReplace the whole profile and its pages and blocks. pages is required, and profile fields you leave out are cleared.
POST/api/v1/resources/{slug}/bio/avatarwriteUpload or replace the profile picture.
DELETE/api/v1/resources/{slug}/bio/avatardeleteRemove the profile picture.
POST/api/v1/resources/{slug}/bio/coverwriteUpload or replace the cover image.
DELETE/api/v1/resources/{slug}/bio/coverdeleteRemove the cover image.
POST/api/v1/resources/{slug}/bio/pages/{pageID}/blocks/filewriteUpload a file and add it to a page as a new image or file block.
POST/api/v1/resources/{slug}/bio/blocks/{blockID}/filewriteReplace the file behind an existing image or file block.
POST/api/v1/resources/{slug}/bio/blocks/{blockID}/thumbnailwriteUpload or replace a block's thumbnail.
GET/api/v1/resources/{slug}/bio/form-submissionsreadList what visitors sent through the page's forms.
GET/api/v1/resources/{slug}/bio/form-submissions/exportreadDownload form submissions as CSV. Cells are escaped so spreadsheets won't run them as formulas.
DELETE/api/v1/resources/{slug}/bio/form-submissions/{submissionID}deletePermanently delete one form submission.

Markdown versions

Method Path Scope Description
GET/api/v1/resources/{slug}/markdown/versionsreadList earlier versions of a Markdown page, newest first. Bidama keeps the last 50.
GET/api/v1/resources/{slug}/markdown/versions/{versionID}readGet the full source of one version.
POST/api/v1/resources/{slug}/markdown/versions/{versionID}/restorewriteMake an earlier version current again. The text it replaces is saved as a version too, so a restore can be undone.

Notion and background jobs

Method Path Scope Description
POST/api/v1/resources/{slug}/file/optimize-originalwriteQueue a job that replaces a large JPEG with a smaller one, or converts HEIC or HEIF to JPEG. This changes the stored original for good.
POST/api/v1/resources/{slug}/notion/refreshwriteQueue a refresh of the whole Notion mirror, or of a single page.
GET/api/v1/resources/{slug}/notion/pagesreadList the cached Notion pages and their refresh status.
GET/api/v1/resources/{slug}/notion/pages/{page_id}readGet one cached page, including its rendered HTML.
GET/api/v1/resources/{slug}/notion/assetsreadList the images and files cached from Notion, with their public URLs.
GET/api/v1/jobsreadList your background jobs. Filter by status and type.
GET/api/v1/jobs/{id}readCheck one job: its status, result or error, attempts, and how long to wait before checking again.
POST/api/v1/jobs/{id}/retrywriteRun a failed or skipped job again. This creates a new job.

Routes that queue work answer 202 Accepted with a job_id, a poll_url, and retry_after_seconds. Poll the job until it finishes. If the same work is already queued you get 409 job_already_active.

Upload requests

An upload request is a private link that lets other people send you files without an account.

Method Path Scope Description
GET/api/v1/upload-requestsreadList your upload requests.
POST/api/v1/upload-requestswriteCreate an upload request. The response is the only time you see its secret token.
GET/api/v1/upload-requests/{id}readGet one upload request and its status.
PATCH/api/v1/upload-requests/{id}writeChange the fields you send. An empty password or expires_at clears it.
POST/api/v1/upload-requests/{id}/statuswriteSet the status to open, paused, or completed.
POST/api/v1/upload-requests/{id}/rotate-tokenwriteReplace the token. The old link stops working, and the new token is shown once.
GET/api/v1/upload-requests/{id}/submissionsreadList the submissions people have sent.
GET/api/v1/upload-requests/{id}/submissions/{submissionID}readGet one submission and the files it created.
PATCH/api/v1/upload-requests/{id}/submissions/{submissionID}writeMark a submission as unreviewed, accepted, needs_changes, or rejected.
POST/api/v1/upload-requests/{id}/revokedeleteShut the upload request down for good.

Analytics

Method Path Scope Description
GET/api/v1/analytics/resources/{slug}readViews and visitors for one resource: a daily series plus top browsers, systems, referrers, UTM tags, countries, and link destinations.
GET/api/v1/analytics/resources/{slug}/exportreadDownload one resource's analytics as CSV.
GET/api/v1/analytics/exportreadDownload analytics for all your resources as CSV.

The summary covers all time unless you pass days (1 to 365) or both from and to as YYYY-MM-DD. Exports cover the last 30 days unless you pass from and to. An export stops at 100,000 rows, and its X-Bidama-Truncated header tells you whether it did.

Webhooks

Method Path Scope Description
GET/api/v1/webhooksreadList your webhook endpoints.
POST/api/v1/webhookswriteAdd an HTTPS endpoint. The response is the only time you see its signing secret.
PATCH/api/v1/webhooks/{id}writeChange the endpoint's name, URL, events, or enabled state.
POST/api/v1/webhooks/{id}/rotate-secretwriteIssue a new signing secret, shown once.
POST/api/v1/webhooks/{id}/enablewriteTurn delivery back on and reset the failure count.
POST/api/v1/webhooks/{id}/disablewritePause delivery.
POST/api/v1/webhooks/{id}/testwriteSend a signed test event.
GET/api/v1/webhooks/{id}/deliveriesreadSee recent deliveries and why any of them failed.

You can subscribe to resource.created, resource.updated, resource.deleted, resource.expired, link.clicked, upload_request.submitted, and upload_request.reviewed. Click events are sent only for links with webhook_clicks turned on. Each delivery is signed. Check X-Bidama-Webhook-Signature, which is v1= followed by the hex HMAC-SHA256 of timestamp + "." + body, using the timestamp from X-Bidama-Webhook-Timestamp.

Creating resources

POST /api/v1/resources takes a JSON body, and the type field decides which other fields apply:

type Required Optional
linktarget_urlmax_views, redirect_rules, test_variants, fallback_url, webhook_clicks, og_title, og_description, og_image_url
markdownbody_markdown (or raw_markdown)max_views
collectionnonelayout, settings
notionsource_url (or fetch_url)root_page_id, auto_refresh, refresh_interval_seconds
biononetheme, settings (with display_name, headline, bio), root_page_slug, root_page_title

Every type also accepts slug, title, description, visibility, password, starts_at, expires_at, and labels. A successful create returns 201 with the same body you would get from GET /api/v1/resources/{slug}.

  • Slugs are 3 to 64 characters of lowercase letters, digits, hyphens, and underscores. Leave out slug and Bidama picks a random 8-character one.
  • visibility is public (the default), unlisted, or private, and can't be changed later. Timestamps are RFC 3339, and starts_at must come before expires_at.
  • You can add up to 20 labels of lowercase letters, digits, hyphens, and underscores.
  • Files and images go through POST /api/v1/uploads as multipart/form-data, with the file in a part called file and the common fields alongside it (except starts_at). max_views works for files but not images. Add type=image or type=file if you want the upload rejected when the content turns out to be the other kind.
  • Creating a Notion mirror also queues its first refresh. If that can't be queued, nothing is created.

HTML artifacts

An artifact is a static website: HTML, CSS, JavaScript, images, and so on. Its share link looks like any other, https://<host>/<slug>. After Bidama checks the password, expiry, and visibility, it sends the visitor to the site on its own isolated origin. Artifacts need to be turned on by the server operator; check features.artifacts_enabled in GET /api/v1/me.

Send multipart/form-data with a text part named metadata that holds a JSON object:

{
  "slug": "optional-slug",
  "title": "Optional title",
  "description": "Optional description",
  "visibility": "public",
  "password": "optional password",
  "expires_at": "2026-08-01T00:00:00Z",
  "labels": ["demo"]
}

Then add the site's files in one of two ways. You can't mix them.

  • As a ZIP. Add one file part named archive.
  • As separate files. List them in metadata.files as {"part": "f0", "path": "index.html"} entries, then add one file part per entry under the name you gave it in part. The path comes from metadata; the uploaded file name is ignored.

The entry page is index.html at the root of the site. If there isn't one, Bidama uses the first .html or .htm file. Either way the page can't be empty. Paths must be plain relative paths: no .., no backslashes, and no two paths that differ only in letter case.

Paths starting with .auth or _bidama are reserved. By default a site can be up to 100 MiB in total, with at most 500 files, no file over 50 MiB, and a ZIP of at most 50 MiB. Your server may set lower limits. max_views isn't supported for artifacts.

To publish a new version, post to /api/v1/resources/{slug}/artifact/deployments. You can send the whole site again the same way, or send a patch: set "mode": "patch" and the current base_deployment_id, upload only the files that changed, and list removed files in delete_paths. Each new version gets a new origin, and the share link always points to the latest one.

Errors

Every error has the same shape:

{
  "error": "Enter a valid URL.",
  "error_code": "validation_failed",
  "field": "target_url"
}

These are the codes you're most likely to see:

error_code HTTP Meaning
invalid_api_key401The key is missing, malformed, unknown, or revoked.
insufficient_scope403The key doesn't have the scope this route needs.
forbidden403The resource belongs to another account.
not_found404Nothing with that slug or ID exists, or it was deleted.
slug_taken409Another resource already uses that slug.
idempotency_conflict409You reused an Idempotency-Key with a different request body.
job_already_active409The same background job is already queued or running.
etag_mismatch412The resource changed since you read it. Fetch it again and retry.
payload_too_large413The request body or upload is over the size limit.
rate_limited429Too many requests. Wait for the number of seconds in Retry-After.
quota_exceeded409This would take you over your storage quota.
validation_failed400A field is invalid. Look at field to see which one.
request_body_invalid400The body isn't valid JSON or multipart data.
unsupported_media415The uploaded file type isn't allowed.
feature_disabled403Your account isn't allowed to use this feature.
service_unavailable503The server is busy or a dependency is down. Retry later.
artifacts_disabled503HTML artifacts are turned off on this server.
artifact_archive_invalid400The ZIP is damaged, encrypted, or expands too much.
artifact_path_invalid400A file path is invalid, reserved, or clashes with another path.
artifact_index_missing400There's no usable HTML entry page, or the root index.html is empty.
artifact_too_large413The site, the ZIP, or one file is over a size limit.
artifact_too_many_files413The site has more files than allowed.

See also: MCP server · OpenAPI document