Skip to content

Generate branded QR codes for print

Generate branded QR codes for print

const logo = await uc.assets.upload(fs.createReadStream('logo.png'), { purpose: 'qr_logo' });

const qr = await uc.qrCodes.create({
  type: 'upi',
  name: 'Counter 1 — UPI',
  payload: { vpa: 'brand@upi', payee_name: 'Brand India Pvt Ltd' },
  design: {
    foreground_color: '#1A1A2E',
    module_shape: 'rounded',
    eye_shape: 'rounded',
    logo: { asset_id: logo.id, size_ratio: 0.2 },
    frame: { style: 'label', text: 'Pay here' },
    error_correction: 'H', // with a logo we raise it to at least Q (ratio ≤ 0.2) or H; never lowered
  },
});

console.log(qr.render_urls.pdf); // print-ready, cacheable URL
console.log(qr.render_urls.svg);
  • render_urls are public, unguessable and cached; hand them straight to your print shop or embed them in an <img>. vCard and UPI images contain contact or payee details, so they are cached for at most a day (1 hour in browsers), and a deleted static vCard or UPI code is removed from our CDN cache.
  • The QR encodes qr.short_url, so you can change the destination later (PATCH /v1/qr-codes/{id} with a new payload) without reprinting.
  • Authenticated renders with design overrides: GET /v1/qr-codes/{id}.png?size=2048&foreground_color=%23000000 (size 512, 1024, 2048 or 4096; other values answer 400 invalid_query). A render that is not ready within 8 seconds answers 503 with Retry-After: 2; retry after that delay.
  • Every saved QR code, static or dynamic, counts toward the plan's QR quota (403 plan_limit_reached, param: dynamic_qr_total); a dynamic code's short link also counts toward links_per_month.
  • Logo, gradient, eye colour, eye shapes other than square, the classy module style and frames (label, badge, speech, card, crop_corner) need Starter or above (403 feature_not_in_plan, param: qr_brand_kit). Logos must be PNG, JPEG or WebP, up to 2 MB, 4096 × 4096 px and 16 megapixels. Uploads are processed one at a time: if another upload is being processed you get 429 rate_limited (param: burst) with Retry-After: 2; retry after that delay.
  • Every design is scan-tested when it is rendered, not when it is saved. A design that does not scan reliably is still saved, but its renders answer 422 qr_unreadable; raise the contrast or remove the logo and update the design.
  • A scanned dynamic UPI code shows a payee confirmation page before opening the UPI app; a dynamic vCard code shows a contact card with Save contact.
  • Deleting a QR code keeps its short link working unless you pass ?delete_link=true.
  • A link that has a QR code keeps its slug: renaming it answers 409 state_conflict (param: slug), because printed codes encode the short URL.
  • Wi-Fi codes have render_urls: null: the image contains the network password, so download it only through the authenticated GET /v1/qr-codes/{id}.png (also .svg, .pdf), which answers Cache-Control: private, no-store. Test-mode codes are not rendered (409 state_conflict).
  • A static code cannot change its payload (422 validation_failed, details[].code: immutable_static_payload); create a new code instead. A vCard website is checked like a link destination (422 destination_blocked or destination_invalid, param: payload.website).
  • Changing a link's destination_url also updates payload.url of its dynamic URL QR code; the render URLs stay the same.
  • The link of a dynamic UPI or vCard code has landing_kind set; change its content on the QR code (PATCH /v1/links/{id} with destination_url answers 422, details[].code: qr_landing_link).
  • Scan analytics: GET /v1/qr-codes/{id}/stats.