OpenMyLink API recipes
Copy-paste examples for the five operations every integration eventually needs: create a short link, create a QR code, fetch click analytics, upload a file, and create a bio link. Each recipe is shown in cURL and Node.js so you can pick the one that fits your stack. Full API reference lives at openmy.link/developers.
This page is a quick-reference cheat sheet for the five operations most integrations care about. Each recipe shows the minimum viable request — no optional parameters — followed by the most useful extensions. For the full schema (every field, every response code, every endpoint), see the live reference at openmy.link/developers.
Auth in one paragraph
Every endpoint takes a bearer token. Get yours from Settings → Developers in the dashboard. Send it on every request:
Authorization: Bearer YOUR_API_KEY
Treat the key like a password. Never commit it. Rotate from the dashboard if it leaks. For server-to-server calls use the API key directly; for user-facing integrations consider the OAuth 2.0 flow instead so each user grants access to their own account.
Recipe 1 — Create a short link
POST/api/url/add — returns the new short URL and an ID you'll need for analytics.
cURL
curl -X POST https://openmy.link/api/url/add \
-H "Authorization: Bearer $OML_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/spring-sale-page",
"custom": "spring-sale",
"utm_source": "newsletter",
"utm_medium": "email",
"utm_campaign": "spring-2026"
}'
Node.js
const res = await fetch('https://openmy.link/api/url/add', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OML_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example.com/spring-sale-page',
custom: 'spring-sale',
utm_source: 'newsletter',
utm_medium: 'email',
utm_campaign: 'spring-2026',
}),
});
const { data } = await res.json();
console.log(data.short_url, data.id);
Useful optional fields: domain (use a branded domain), password (gate the link), expires_at (auto-expire), pixel_ids (array of tracking pixel IDs to fire on every click).
Recipe 2 — Create a QR code
POST/api/qr/add — returns a downloadable PNG/SVG plus a tracking ID. Pass type=dynamic if you want to be able to repoint the destination later (see Edit QR codes after printing).
cURL
curl -X POST https://openmy.link/api/qr/add \
-H "Authorization: Bearer $OML_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q2 menu",
"type": "dynamic",
"url": "https://example.com/menu",
"size": 600,
"foreground": "#07111F",
"background": "#FFFFFF",
"format": "png"
}'
Node.js
const res = await fetch('https://openmy.link/api/qr/add', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OML_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Q2 menu',
type: 'dynamic',
url: 'https://example.com/menu',
size: 600,
foreground: '#07111F',
background: '#FFFFFF',
format: 'png',
}),
});
const { data } = await res.json();
// data.image_url is a hosted PNG you can embed or download
// data.id is the tracking ID — keep it for analytics calls
Recipe 3 — Fetch click analytics
GET/api/url/:id/stats for a short link, GET/api/qr/:id/stats for a QR. Returns aggregated counts and a daily breakdown for whatever range you pass.
cURL
curl https://openmy.link/api/url/12345/stats?from=2026-05-01&to=2026-05-31 \
-H "Authorization: Bearer $OML_KEY"
Node.js
const params = new URLSearchParams({ from: '2026-05-01', to: '2026-05-31' });
const res = await fetch(
`https://openmy.link/api/url/12345/stats?${params}`,
{ headers: { 'Authorization': `Bearer ${process.env.OML_KEY}` } }
);
const { data } = await res.json();
// data.total_clicks, data.daily[], data.by_country{}, data.by_device{}, etc.
For a full per-event firehose (every click, with timestamp + IP-derived geo + UTM tags + referrer), pass granularity=event. The default is daily aggregate, which is what you want for dashboards.
Recipe 4 — Upload a file
POST/api/file/upload — multipart form upload. Returns a hosted, trackable download URL plus an ID you can use to replace the file later without changing the URL.
cURL
curl -X POST https://openmy.link/api/file/upload \
-H "Authorization: Bearer $OML_KEY" \
-F "file=@./Q2-pricing-deck.pdf" \
-F "name=Q2 pricing deck" \
-F "domain=files.acme.com"
Node.js
import fs from 'node:fs';
import { FormData } from 'undici';
const form = new FormData();
form.append('file', new Blob([fs.readFileSync('./Q2-pricing-deck.pdf')]), 'Q2-pricing-deck.pdf');
form.append('name', 'Q2 pricing deck');
form.append('domain', 'files.acme.com');
const res = await fetch('https://openmy.link/api/file/upload', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.OML_KEY}` },
body: form,
});
const { data } = await res.json();
// data.download_url is the public branded URL
// data.id lets you swap the file later via PATCH /api/file/:id
To replace the underlying file without changing the URL: PATCH /api/file/:id with a new multipart upload. Everyone who already has the link keeps using the same URL.
Recipe 5 — Create a bio-page block
Bio pages are composed of blocks (links, headings, images, etc.). POST/api/bio/:bio_id/blocks appends a block.
cURL
curl -X POST https://openmy.link/api/bio/789/blocks \
-H "Authorization: Bearer $OML_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "link",
"title": "Spring Sale — 30% off",
"url": "https://example.com/spring-sale",
"icon": "tag",
"position": 2
}'
Node.js
const res = await fetch('https://openmy.link/api/bio/789/blocks', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OML_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'link',
title: 'Spring Sale — 30% off',
url: 'https://example.com/spring-sale',
icon: 'tag',
position: 2,
}),
});
const { data } = await res.json();
console.log('Added block', data.id);
The full block catalog (48+ types: link, featured-link, video, audio, product, calendar, form, etc.) is on the bio pages product page. Each type takes a slightly different payload — see the live reference for the schema of each.
Error handling pattern
Every response carries an error field. 0 means success; anything else is a failure with a human-readable message:
{
"error": 0,
"data": { "id": 12345, "short_url": "https://oml.link/abc" }
}
{
"error": 1,
"message": "Custom alias 'spring-sale' is already in use"
}
Check error !== 0 on every response. Don't trust the HTTP status alone — some validation errors return HTTP 200 with an error body, and the error field is always authoritative.
Rate limits
The default is 30 requests per minute. Every response returns three rate-limit headers:
X-RateLimit-Limit— your current ceiling.X-RateLimit-Remaining— calls left in the window.X-RateLimit-Reset— UNIX timestamp when the window resets.
On 429 responses, sleep until X-RateLimit-Reset and retry. Don't immediately retry — that's how you turn a soft limit into a hard ban.
Full reference and additional endpoints (campaigns, channels, pixels, domains, OAuth) at openmy.link/developers.
Was this article helpful?
Tell us what's working and what isn't — we read every reply.