Developer API
Create and update QR codes, read scan stats, collect form entries and redeem coupons from your own system.
Base URL: https://myinkqr.com/api/v1. Requests and responses are JSON in UTF-8, and times are ISO 8601. Error messages follow the Accept-Language header: en, ms or zh-hans.
Getting access
API access is turned on for each account by the site administrator.
- Log in and open the API keys page from your account.
- Press Request API access. You get an email when it's on.
- Create a key and copy it right away. It's shown only once.
Authentication
Send your key in the Authorization header on every request.
Authorization: Bearer qrk_your_key_here
Each key can be limited when you create it:
- Read only allows GET requests only. Use it for reports and dashboards.
- Allowed IPs makes the key work only from the addresses or ranges you list, such as
203.0.113.5or203.0.113.0/24. - Expiry date stops the key after that day.
Check a key and your account:
GET /api/v1/
{"account": "kopicorner", "plan": "pro", "qr_count": 12, "qr_limit": null,
"key": {"name": "POS", "scope": "write", "expires": null},
"rate_limit_per_minute": 120, "kinds": ["url", "links", "vcard", "..."],
"docs": "https://myinkqr.com/developers/"}
Errors
Errors return a readable message and a stable code to check in your program.
{"error": "This API key is read-only", "code": "read_only"}
| HTTP | code | Meaning |
|---|---|---|
| 401 | missing_key | No Authorization header. |
| 401 | invalid_key | The key doesn't exist or was deleted. |
| 401 | key_expired | The key is past its expiry date. |
| 403 | api_disabled | API access isn't turned on for this account. |
| 403 | account_blocked | The account is suspended, unverified or waiting for approval. |
| 403 | ip_not_allowed | The request came from an IP the key doesn't allow. |
| 403 | read_only | A read-only key tried to change something. |
| 403 | plan_limit | The free plan limit was reached, or the feature needs Pro. |
| 400 | invalid_json | The body isn't a JSON object. |
| 400 | invalid_content | Content failed validation. See the fields object. |
| 400 | bad_url | The URL isn't accepted: not https, a short link, or flagged as unsafe. |
| 400 | unsupported_kind | This QR type can't be created or edited through the API. |
| 404 | not_found | No such item in your account. |
| 405 | method_not_allowed | Wrong HTTP method for this endpoint. |
| 409 | not_allowed | The code can't be redeemed: already used, expired or not enough stamps. |
| 429 | rate_limited | Too many requests. Wait a minute and try again. |
Rate limits
Each key can make 120 requests per minute unless the administrator sets a different limit for your account. Each IP address is also limited to 300 requests per minute. You can create up to 200 QR codes a day and change destinations up to 30 times an hour. The free plan includes 3 QR codes.
QR codes
| Method | Path | What it does |
|---|---|---|
| GET | /qr/ | List your QR codes. Filters: page, page_size (up to 100), q (name, code or URL), kind, active=true|false. |
| POST | /qr/ | Create a QR code. |
| GET | /qr/{id}/ | Get one QR code. |
| PATCH | /qr/{id}/ | Change name, active, target_url (URL type) or content (other types). |
| DELETE | /qr/{id}/ | Delete a QR code. Printed copies stop working. |
Create a URL QR code
POST /api/v1/qr/
{"name": "November flyer", "target_url": "https://kopicorner.my/menu"}
201 Created
{"id": "AbC123", "kind": "url", "name": "November flyer",
"target_url": "https://kopicorner.my/menu", "content": {},
"short_url": "https://go.example.my/r/AbC123", "active": true, "blocked": false,
"folder": null, "scans": 0, "created": "2026-11-05T10:00:00+08:00"}
Change where it goes
PATCH /api/v1/qr/AbC123/
{"target_url": "https://kopicorner.my/menu-december"}
Every destination is checked for scams and malware before it's saved. Lists return page, page_size, total, has_more and results.
QR types and content
Set kind and send a content object. PDF and image gallery QR codes need file uploads, so create those on the website.
POST /api/v1/qr/
{"name": "Ali's card", "kind": "vcard",
"content": {"first_name": "Ali", "last_name": "Tan", "mobile": "+60123456789"} }
When a field is wrong you get invalid_content and the problem for each field:
{"error": "...", "code": "invalid_content", "fields": {"mobile": ["Enter a valid phone number"]} }
| kind | Type | content fields |
|---|---|---|
url | URL | Use target_url instead of content. |
links | Link page | title* text, description long, label_1 text, url_1 url, label_2 text, url_2 url, label_3 text, url_3 url, label_4 text, url_4 url, label_5 text, url_5 url, label_6 text, url_6 url, label_7 text, url_7 url, label_8 text, url_8 url |
vcard | Digital business card | first_name* text, last_name text, company text, job_title text, phone tel, mobile tel, email email, website url, address text, note long |
text | Text | title text, body* long |
app | App download | app_name* text, ios_url url, android_url url, fallback_url url |
location | Location | address* text, map_url url |
whatsapp | number* tel, message long | |
email | to* email, subject text, body long | |
sms | SMS | number* tel, message long |
phone | Phone | number* tel, title text |
event | Event | title* text, start* datetime, end datetime, venue text, description long, url url |
coupon | Coupon | title* text, description long, code text, valid_until date, terms long, url url |
feedback | Feedback | title* text, description long |
form | Form | title* text, description long, form_fields* long, consent long, success text, notify_email email, one_per_contact bool |
wifi | Wi-Fi | ssid* text, security* choice, wifi_password text, hidden bool |
* required.
Stats and images
| Method | Path | What it does |
|---|---|---|
| GET | /qr/{id}/stats/?days=30 | Totals, daily counts, and top countries, cities, devices, systems and browsers. Up to 365 days on Pro and 7 days on Free. |
| GET | /qr/{id}/image.png?size=1000 | PNG image, 128 to 2000 pixels wide. |
| GET | /qr/{id}/image.svg | SVG for print (Pro). |
{"id": "AbC123", "days": 30, "total": 812, "unique": 640,
"daily": [{"date": "2026-11-01", "scans": 31}],
"countries": [{"country": "MY", "scans": 790}],
"cities": [{"city": "George Town", "scans": 402}],
"devices": [{"device": "Mobile", "scans": 760}], "os": [], "browsers": []}
Form entries
GET /qr/{id}/submissions/ returns entries from a Form QR code, newest first.
{"page": 1, "page_size": 100, "total": 1, "has_more": false,
"results": [{"id": 51, "created": "2026-11-05T12:30:00+08:00",
"data": {"Name": "Ali", "Phone": "0123456789"} }]}
Form entries are personal data. Keep them safe and delete them when you no longer need them, as the PDPA requires.
Unique codes
| Method | Path | What it does |
|---|---|---|
| GET | /codes/ | Your batches of unique codes. |
| GET | /codes/{batch_id}/?status=active | Codes in a batch. status is active, used or void. |
| GET | /code/{code}/ | One code. |
| POST | /code/{code}/redeem/ | Redeem a coupon or check in a ticket. |
| POST | /code/{code}/stamp/ | Add a loyalty stamp. |
| POST | /code/{code}/claim/ | Give the loyalty reward. |
A cash register can redeem a coupon in one call. A code can only be redeemed once, even if two tills try at the same moment.
POST /api/v1/code/7KQ2M9XW4P/redeem/
200 {"message": "Redeemed", "item": {"code": "7KQ2M9XW4P", "serial": "A-0001", "status": "used", "uses": 1} }
409 {"error": "This code has already been used", "code": "not_allowed", "item": {"status": "used"} }
Code examples
curl
curl https://myinkqr.com/api/v1/qr/ \
-H "Authorization: Bearer $QR_API_KEY"
curl -X POST https://myinkqr.com/api/v1/qr/ \
-H "Authorization: Bearer $QR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "November flyer", "target_url": "https://kopicorner.my/menu"}'
Python
import os
import requests
API = "https://myinkqr.com/api/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["QR_API_KEY"]}
qr = requests.post(f"{API}/qr/", headers=HEADERS, timeout=15,
json={"name": "November flyer", "target_url": "https://kopicorner.my/menu"}).json()
print(qr["short_url"])
stats = requests.get(f"{API}/qr/{qr['id']}/stats/", headers=HEADERS, params={"days": 30}, timeout=15).json()
print(stats["total"], "scans")
JavaScript (Node.js 18 or newer)
const API = "https://myinkqr.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.QR_API_KEY}` };
const res = await fetch(`${API}/code/7KQ2M9XW4P/redeem/`, { method: "POST", headers });
const body = await res.json();
if (res.ok) console.log("Redeemed", body.item.serial);
else console.log("Not redeemed:", body.code);
PHP
$ch = curl_init("https://myinkqr.com/api/v1/qr/AbC123/");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("QR_API_KEY"), "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["target_url" => "https://kopicorner.my/menu-december"]),
CURLOPT_RETURNTRANSFER => true,
]);
$qr = json_decode(curl_exec($ch), true);
Keeping keys safe
- Call the API from your server. A key placed in a web page or mobile app can be read by anyone.
- Use a read-only key for reports and dashboards.
- Limit each key to your server's IP address.
- Give each system its own key, so you can delete one without stopping the others.
- If a key leaks, delete it on the API keys page. It stops working immediately.