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.

  1. Log in and open the API keys page from your account.
  2. Press Request API access. You get an email when it's on.
  3. 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.5 or 203.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"}
HTTPcodeMeaning
401missing_keyNo Authorization header.
401invalid_keyThe key doesn't exist or was deleted.
401key_expiredThe key is past its expiry date.
403api_disabledAPI access isn't turned on for this account.
403account_blockedThe account is suspended, unverified or waiting for approval.
403ip_not_allowedThe request came from an IP the key doesn't allow.
403read_onlyA read-only key tried to change something.
403plan_limitThe free plan limit was reached, or the feature needs Pro.
400invalid_jsonThe body isn't a JSON object.
400invalid_contentContent failed validation. See the fields object.
400bad_urlThe URL isn't accepted: not https, a short link, or flagged as unsafe.
400unsupported_kindThis QR type can't be created or edited through the API.
404not_foundNo such item in your account.
405method_not_allowedWrong HTTP method for this endpoint.
409not_allowedThe code can't be redeemed: already used, expired or not enough stamps.
429rate_limitedToo 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

MethodPathWhat 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"]} }
kindTypecontent fields
urlURLUse target_url instead of content.
linksLink pagetitle* 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
vcardDigital business cardfirst_name* text, last_name text, company text, job_title text, phone tel, mobile tel, email email, website url, address text, note long
textTexttitle text, body* long
appApp downloadapp_name* text, ios_url url, android_url url, fallback_url url
locationLocationaddress* text, map_url url
whatsappWhatsAppnumber* tel, message long
emailEmailto* email, subject text, body long
smsSMSnumber* tel, message long
phonePhonenumber* tel, title text
eventEventtitle* text, start* datetime, end datetime, venue text, description long, url url
couponCoupontitle* text, description long, code text, valid_until date, terms long, url url
feedbackFeedbacktitle* text, description long
formFormtitle* text, description long, form_fields* long, consent long, success text, notify_email email, one_per_contact bool
wifiWi-Fissid* text, security* choice, wifi_password text, hidden bool

* required.

Stats and images

MethodPathWhat it does
GET/qr/{id}/stats/?days=30Totals, 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=1000PNG image, 128 to 2000 pixels wide.
GET/qr/{id}/image.svgSVG 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

MethodPathWhat it does
GET/codes/Your batches of unique codes.
GET/codes/{batch_id}/?status=activeCodes 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.