Skip to main content

Decode and encode MergeID electronic invoice QR codes

Project description

qrid — Python

Decode and encode MergeID electronic invoice QR codes.

Python port of qrid/codec (PHP). All three implementations (PHP, Node.js, Python) share the same payload format and function signatures, so QR codes generated by any one of them scan correctly in the others.

Typical usage flow

sequenceDiagram
    actor Staff as Billing Staff
    participant ERP as Billing / ERP System
    participant Lib as qrid
    participant QR as Invoice QR Code
    participant App as MergeID App

    Staff->>ERP: create invoice
    ERP->>Lib: encode_qr_id(id, company, email, address, activity_code)
    Lib-->>ERP: SVG QR code
    ERP->>QR: print / embed on invoice

    Note over App,QR: later, at point of scan
    App->>QR: scan with camera
    QR-->>App: base64 payload string
    App->>Lib: decode_qr_id(encoded)
    Lib-->>App: { v, id, company, email, address, activity_code }
    App-->>Staff: display verified invoice identity

Installation

# Decode only (no extra dependencies)
pip install qrid

# Decode + encode SVG
pip install 'qrid[encode]'

Usage

Decode

from qrid import decode_qr_id

# `encoded` is the raw string value scanned from a MergeID QR code
payload = decode_qr_id(encoded)

print(payload["v"])             # Payload schema version (int, currently 1)
print(payload["id"])            # Tax or company ID              (e.g. "3101679980")
print(payload["company"])       # Company legal name
print(payload["email"])         # Billing e-mail address
print(payload["address"])       # Physical address
print(payload["activity_code"]) # Installation / activity code (e.g. "ACT-001"), or "" if blank

decode_qr_id strips surrounding whitespace before decoding, so strings copied with accidental padding are handled transparently.

Exceptions raised:

Exception Cause
ValueError Input is not valid base64
json.JSONDecodeError Decoded bytes are not valid JSON
import json
from qrid import decode_qr_id

try:
    payload = decode_qr_id(raw)
except ValueError:
    # QR data was not base64
    ...
except json.JSONDecodeError:
    # QR data decoded but was not the expected JSON structure
    ...

Encode (requires qrid[encode])

from qrid import encode_qr_id

svg = encode_qr_id(
    id="3101679980",
    company="Acme Corp S.A.",
    email="billing@acme.example",
    address="123 Main St, San José, Costa Rica",
    activity_code="ACT-001",
)

# Write to a file
with open("invoice_qr.svg", "w") as f:
    f.write(svg)

# Or serve directly
# Content-Type: image/svg+xml

activity_code is optional and defaults to "" (blank). A blank activity code signals a consuming system to generate an electronic ticket instead of using an activity code.

Exceptions raised:

Exception Cause
ImportError segno is not installed (pip install 'qrid[encode]')

Payload format

The QR code data is a UTF-8 JSON object encoded as standard base64 (no line-breaks):

{
  "v": 1,
  "id": "3101679980",
  "company": "Acme Corp S.A.",
  "email": "billing@acme.example",
  "address": "123 Main St, San José, Costa Rica",
  "activity_code": "ACT-001"
}
Field Type Description
v int Payload schema version. Currently always 1.
id str Tax / company registration ID.
company str Legal company name (UTF-8, including accented characters).
email str Primary billing or contact e-mail address.
address str Physical address of the company.
activity_code str Installation or activity code that links the QR to an internal record. May be blank (""), which signals a consuming system to generate an electronic ticket instead.

Requirements

Dependency Version Required for
Python >= 3.9 Always
segno >= 1.6 encode_qr_id() only

Running tests

pip install 'qrid[dev]'
pytest

Publishing

Releases to PyPI are fully automated — there is no manual twine upload step:

  1. Bump version in pyproject.toml and merge to main.
  2. release.yml runs on every push to main. It reads the version from pyproject.toml; if no vX.Y.Z tag already exists for it, it runs the test suite and creates that tag plus a GitHub Release.
  3. In that same run, release.yml dispatches publish.yml via the GitHub API (gh workflow run publish.yml), which re-runs the tests, builds the sdist/wheel, and uploads to PyPI using Trusted Publishing (OIDC) — no stored API token.

publish.yml is dispatched as an independent, top-level run via the Actions API rather than invoked as a reusable workflow (workflow_call) or relying on the release: published event. Both alternatives were tried and both are broken for this use case:

  • release: published never fires for releases created with the Actions-internal GITHUB_TOKEN (GitHub's anti-recursion guard).
  • workflow_call chaining runs publish.yml as a job within release.yml's own run, so the OIDC certificate's build-config claim identifies the caller (release.yml) instead of publish.yml — PyPI's Trusted Publisher verification then rejects the upload outright (does not match expected Trusted Publisher). PyPI's own docs confirm reusable workflows are not supported for Trusted Publishing.

A workflow_dispatch API call made with GITHUB_TOKEN is explicitly exempt from the anti-recursion guard and starts a genuine new top-level run of publish.yml, so its OIDC certificate correctly identifies publish.yml as the source. publish.yml also still accepts release: published (for a release cut by hand) and workflow_dispatch (manual re-run) as a fallback.

Pushes to main that don't change the version are a no-op for release.yml (the tag already exists), so unrelated commits (docs, CI tweaks) don't trigger a release.

One-time PyPI setup (already done for this project, documented here for reference): add a pending trusted publisher at pypi.org/manage/account/publishing with owner Quality-XP-Development-SESSA, repository qrid-python, workflow filename publish.yml, and environment name pypi; the same pypi environment must exist under the repo's GitHub Settings → Environments.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qrid-1.1.0.tar.gz (6.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

qrid-1.1.0-py3-none-any.whl (5.2 kB view details)

Uploaded Python 3

File details

Details for the file qrid-1.1.0.tar.gz.

File metadata

  • Download URL: qrid-1.1.0.tar.gz
  • Upload date:
  • Size: 6.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for qrid-1.1.0.tar.gz
Algorithm Hash digest
SHA256 1472b2580db0098c81a2377b3f01e03c466df621f7b1786f865462b9b52823e6
MD5 babf4c6e16b1af125ffb62a58af6b133
BLAKE2b-256 f1d4e5003da527d4866e4847605cc4f440a97612c3dcf1106e8cae64ee363d5c

See more details on using hashes here.

Provenance

The following attestation bundles were made for qrid-1.1.0.tar.gz:

Publisher: publish.yml on Quality-XP-Development-SESSA/qrid-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file qrid-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: qrid-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 5.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for qrid-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0ed79c01130037025226cd23e5aa569320a682404b805e2e1741c8f50526c4ba
MD5 8735222f7f1d20b32c050d45aaa9bd3a
BLAKE2b-256 de94ab7e2d24c24bd7f5eff59dddbd26c5f479ae1060efb1bd61790337519ea8

See more details on using hashes here.

Provenance

The following attestation bundles were made for qrid-1.1.0-py3-none-any.whl:

Publisher: publish.yml on Quality-XP-Development-SESSA/qrid-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page