Skip to content

REST API quickstart ​

The API is at https://api.dynm.link, every path under /v1, JSON in and out. Every field of every schema is in the REST API reference and the raw OpenAPI document.

Authentication ​

Send an API key as a Bearer token:

http
Authorization: Bearer $DYNM_API_KEY

An x-api-key: $DYNM_API_KEY header works too. Create a key on the dashboard's API keys page, or with dynm.link login (see the CLI). A key that is invalid, expired or revoked gets 401 UnauthorizedError.

bash
curl https://api.dynm.link/v1/microlinks \
  -H "Authorization: Bearer $DYNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"redirect","content":{"web":"https://example.com"}}'

201 returns the link. Its id is what every other link endpoint takes:

json
{
  "id": "01K6H2M9V7Q4XJ8C3N5R2T6W1Y",
  "host": "alice.dynm.link",
  "slug": "k7m2qp",
  "url": "https://alice.dynm.link/k7m2qp",
  "type": "redirect",
  "content": { "web": "https://example.com", "rules": [], "preview": "follow" },
  "title": null,
  "description": null,
  "thumbnail": null,
  "expiresAt": 1762041600000,
  "createdAt": 1759449600000,
  "updatedAt": 1759449600000
}

The other create fields are optional:

  • host — the folder: your {username}.dynm.link subdomain when omitted, or one of your active custom domains.
  • slug — the path in the folder; generated when omitted. Lowercase letters, digits and single hyphens; / separates segments, a trailing * matches the rest of the path, and an empty slug publishes at the folder's root.
  • expiresAt — when the link stops resolving, in epoch milliseconds; null never. Omitted, 30 days from now.
  • title, description, thumbnail — the link's social card (see below).

type decides the shape of content. Every URL must be http or https, except a bio link's, which may also be a mailto: or tel:.

typecontent
redirect{ web, rules?, preview? } — web is where visitors go when no rule matches
text{ body, markdown? } — markdown: true renders the body as a page
bio{ name, headline?, avatar?, links: [{ label, url }] }
file{ r2Key, filename } — an upload; the response adds size and mime

Routing rules ​

A redirect sends each visitor to the first rule whose when clauses all match, else to web. A clause is { op, field, value, negate? }: op is eq, lt, lte, gt or gte (or in, with values instead of value); field is platform (ios, android or other), country (ISO 3166 alpha-2) or now (epoch milliseconds).

bash
curl https://api.dynm.link/v1/microlinks \
  -H "Authorization: Bearer $DYNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"redirect","slug":"app","content":{
        "web":"https://example.com",
        "rules":[
          {"when":[{"op":"eq","field":"platform","value":"ios"}],"to":"https://apps.apple.com/app/id000000000"},
          {"when":[{"op":"in","field":"country","values":["US","CA"]}],"to":"https://example.com/na"}
        ]}}'

Social preview on a redirect ​

By default a redirect passes crawlers straight through to its destination, so pasting the short link in Slack, iMessage or X shows the destination page's own card. Set content.preview to custom to serve your own card instead:

bash
curl https://api.dynm.link/v1/microlinks \
  -H "Authorization: Bearer $DYNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"redirect","content":{"web":"https://example.com","preview":"custom"},
       "title":"Launch week","description":"Everything shipping this week"}'

The card image (thumbnail) is the r2Key of an upload. The two are exclusive: title, description or thumbnail on a redirect whose preview is follow returns 400 PreviewConflictError. To drop a custom card, set preview back to follow and those fields to null in the same request.

Upload a file ​

A file goes straight to storage; the API says where. With jq to build and read the JSON:

bash
FILE=brand-kit.zip MIME=application/zip

# 1. Start the upload: the file's name, size in bytes and type.
UPLOAD=$(curl -s https://api.dynm.link/v1/uploads \
  -H "Authorization: Bearer $DYNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg filename "$FILE" --arg mime "$MIME" --argjson size "$(wc -c < "$FILE")" \
        '{$filename, $size, $mime}')")
# → { "r2Key": "uploads/…/brand-kit.zip", "uploadUrl": "https://…", "maxBytes": 52428800 }

# 2. PUT the bytes to uploadUrl, with the file's Content-Type and no API key.
curl -X PUT "$(jq -r .uploadUrl <<<"$UPLOAD")" \
  -H "Content-Type: $MIME" \
  --data-binary @"$FILE"

# 3. Create a file link that references the r2Key.
curl https://api.dynm.link/v1/microlinks \
  -H "Authorization: Bearer $DYNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg r2Key "$(jq -r .r2Key <<<"$UPLOAD")" --arg filename "$FILE" \
        '{type: "file", content: {$r2Key, $filename}}')"
  • uploadUrl carries its own authorization and is good for an hour. The Content-Type you PUT is what the file is served as.
  • There is no confirm step: creating the link claims the upload and checks the stored file's real size against your plan (413 FileTooLargeError). An upload no link references is deleted after a day.
  • A file link's slug is a plain one: no /, no *, not the root.
RequestDoes
GET /v1/microlinksYour links, newest first, 25 a page (limit up to 100). Pass nextCursor back as cursor for the next page; it is null on the last.
GET /v1/microlinks/find?host=…&slug=…Your link at a folder and slug.
GET /v1/microlinks/slug-availability?host=…&slug=…{ "available": true }, or { "available": false, "reason", "message" } with reason invalid, reserved or taken.
GET /v1/microlinks/{id}One link.
PATCH /v1/microlinks/{id}Sets only the fields given; null clears an optional one. New content replaces the old whole and must be the link's own type's.
DELETE /v1/microlinks/{id}Deletes the link, with its file and card image.
GET /v1/microlinks/{id}/qrThe short URL as an SVG QR code.
bash
curl -X PATCH https://api.dynm.link/v1/microlinks/$LINK_ID \
  -H "Authorization: Bearer $DYNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Launch week","expiresAt":null}'

Analytics ​

  • GET /v1/microlinks/{id}/stats?range=7d — views, previews, downloads and bio clicks per UTC day, with breakdowns by referrer, country, device, routing rule, bio link and query parameter. range is 24h, 7d (the default), 30d or 12mo. Free accounts get the 7-day range only, without breakdowns (depthLocked: true).
  • POST /v1/microlinks/stats/views with { "ids": [...] } (1 to 100) — the last 7 days of views for each of those links you own.

Query parameters ​

  • Passthrough: a redirect forwards the query parameters it is visited with to its destination. A parameter on both replaces the destination's value.
  • Segments: every query parameter is recorded as an analytics dimension, so ?src=yt or ?campaign=launch splits your visits.
  • Reserved: parameters whose decoded key starts with _ are never forwarded or recorded.
  • QR codes: the QR encodes the bare short URL. To count QR scans separately, print a QR of the link with your own tag, e.g. ?src=qr.

Custom domains ​

A domain you own serves links as a folder of its own (Pro: on Free, adding one returns 403 ProRequiredError with wall: "custom-domains").

bash
curl https://api.dynm.link/v1/domains \
  -H "Authorization: Bearer $DYNM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"links.example.com"}'

The domain starts pending. Publish a TXT record named verifyName with the value verifyValue, and a CNAME from the hostname to cnameTarget. It turns active once both are in place and its certificate is issued; pending domains are checked in the background, and POST /v1/domains/{id}/check checks now and reports what DNS showed (verification). Then create links in it with "host": "links.example.com".

GET /v1/domains lists yours, GET /v1/domains/{id} reads one, and DELETE /v1/domains/{id} removes it along with its links.

Account ​

GET /v1/me returns your account: username, plan (tier), its limits (null is unlimited) and your usage against them.

Without an API key ​

POST /v1/microlinks and POST /v1/uploads also work with no credential. The link lands on the shared dynm.link apex with a generated slug and expires after 30 days; bio pages and chosen slugs need an account (403 SignInRequiredError). Anonymous uploads are capped at 50 MB, and links and uploads share a per-IP hourly cap (429 RateLimitedError).

Errors ​

Every error body carries a code to match on, a message for a person, and the error's own fields:

json
{ "code": "SlugTakenError", "message": "alice.dynm.link/launch is taken.", "slug": "launch" }
StatuscodeFieldsWhen
400InvalidRequestErrorThe body, query or path doesn't match the schema; message names the field.
400InvalidSlugErrorslugA reserved slug, or a shape the folder doesn't take.
400SelfRedirectErrorA redirect that would point back at itself.
400PreviewConflictErrorA custom card on a redirect whose preview is follow.
400InvalidContentErrorNew content that isn't the link's own type's.
400InvalidDomainErrorA hostname that can't be served as a custom domain.
401UnauthorizedErrorNo API key, or one that is invalid, expired or revoked.
403ProRequiredErrorwallA plan limit: links, storage or custom-domains.
403SignInRequiredErrorAn anonymous caller asked for a bio page or a chosen slug.
404MicrolinkNotFoundErrorNo link of yours with that id, or at that host and slug.
404FolderNotFoundErrorhosthost isn't your subdomain or one of your domains.
404UploadNotFoundErrorr2KeyNo upload of yours with that key.
404DomainNotFoundErrorNo domain of yours with that id.
409SlugTakenErrorslugThe slug is taken in that folder.
409UploadInUseErrorr2KeyAnother link already uses that upload, or it's this link's file and card image at once. Upload the file again.
409DomainAlreadyAddedErroridYou've already added this hostname; id is that domain's.
409AccountSetupRequiredErrorThe account hasn't claimed its username on the dashboard yet.
413FileTooLargeErrormaxBytesThe file is over your per-file limit.
429RateLimitedErrorAnonymous use from this IP hit its hourly cap.

The reference lists the errors each endpoint can return.

One short link: redirect, page, file, or QR.