Browser Implementation Guide
You can build a fully working Work Card scanning system using only a web browser — no native iOS or Android app required. This page covers both the QR code and NFC card approaches, the Ergani API call, and how to handle the 15-minute submission window when connectivity drops.
The browser camera API and Web NFC API are available in modern browsers. A simple web app deployed on your server is enough to run a professional card scanner on a tablet at the entrance.
How it works
Two sides:
- Worker — shows their QR code on their phone, or taps a physical NFC card on the reader.
- Employer tablet — reads the QR/NFC, maps it to the employee's ΑΦΜ, and posts the arrival or departure event to the Ergani API within 15 minutes.
Worker's phone Employer's tablet Your server Ergani API
────────────── ───────────────────── ─────────── ──────────
Shows QR/NFC → Reads card or QR code → POST /workcard → WRKCardSE
Looks up employee ΑΦΜ Greece timezone Returns OK
Shows ✅ on screen Handles JWT auth
QR Code approach
Works on any device — iPhone, Android, desktop.
Worker side — show the QR
Generate a QR code from the employee's ΑΦΜ and display it in their browser.
npm install qrcode
import QRCode from 'qrcode'
async function showWorkerQR(afm) {
const canvas = document.getElementById('qr-canvas')
await QRCode.toCanvas(canvas, afm, { width: 300 })
}
The worker opens this page on their phone and holds it up at the entrance.
Employer side — scan with the camera
npm install html5-qrcode
import { Html5QrcodeScanner } from 'html5-qrcode'
const scanner = new Html5QrcodeScanner('reader', { fps: 10, qrbox: 250 })
scanner.render((afm) => {
// afm is the employee's ΑΦΜ read from the QR
handleScan(afm)
})
NFC Card approach
Workers get a cheap blank NFC card (€0.20–0.50 each). They tap it on the employer's Android tablet — no phone needed.
The Web NFC API (NDEFReader) works only on Chrome on Android. It is not supported on iOS — use QR as a fallback for iPhone users.
async function startNFCScanner() {
if (!('NDEFReader' in window)) {
showError('NFC not supported — please use QR code instead')
return
}
const reader = new NDEFReader()
await reader.scan() // prompts for NFC permission
reader.addEventListener('reading', ({ serialNumber }) => {
// serialNumber is the unique ID burned into the card
// e.g. "04:A3:2F:11:BC:44:80"
handleScan(serialNumber)
})
}
You store a mapping of cardId → employee ΑΦΜ in your database. When a card is tapped, look up the employee and proceed.
QR vs NFC
| QR Code | NFC Card | |
|---|---|---|
| Works on iPhone | ✅ | ❌ |
| Speed | Slower (aim camera) | Faster (just tap) |
| Card cost | Free (worker uses phone) | €0.20–0.50 per card |
| Employer device | Any phone or tablet | Android only |
| Feels professional | OK | ✅ Yes |
Recommendation: support both. Workers without a smartphone use the NFC card. The Ergani API call is identical either way.
Calling the Ergani API
Once you have the employee's ΑΦΜ, post the event from your backend. Never call the Ergani API directly from the browser — keep your JWT token server-side.
// Browser → your server
async function handleScan(employeeId) {
const employee = await getEmployeeByCardId(employeeId) // your DB lookup
if (!employee) { showError('Unknown card'); return }
const eventType = getNextEventType(employee) // 'arrival' or 'departure'
await fetch('/api/workcard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
afm: employee.afm,
branchId: currentBranch.id,
type: eventType,
timestamp: new Date().toISOString()
})
})
showSuccess(`${employee.name} — ${eventType}`)
}
// Your server → Ergani API
app.post('/api/workcard', async (req, res) => {
const { afm, branchId, type, timestamp } = req.body
const payload = {
f_afm_ergodoti: employer.afm,
f_aa_pararthmatos: branchId,
f_afm_ergazomenos: afm,
f_date: toGreekLocalTime(timestamp), // ⚠️ see note below
f_type: type === 'arrival' ? '0' : '1'
}
const response = await fetch(
'https://eservices.yeka.gr/WebservicesAPI/Api/Documents/WRKCardSE',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${erganiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
}
)
res.json(await response.json())
})
f_date must be in Greece local time (UTC+2 in winter, UTC+3 in summer with DST). Sending UTC causes submissions to appear late and get rejected. Convert before sending:
function toGreekLocalTime(isoString) {
return new Date(isoString).toLocaleString('sv-SE', {
timeZone: 'Europe/Athens'
}).replace(' ', 'T')
}
Offline queue — handling the 15-minute window
If the internet drops mid-shift, store scans locally and retry when connectivity returns.
async function submitWithFallback(payload) {
try {
await callErganiAPI(payload)
} catch {
const queue = JSON.parse(localStorage.getItem('wc_queue') || '[]')
queue.push({ payload, queuedAt: Date.now() })
localStorage.setItem('wc_queue', JSON.stringify(queue))
showWarning('Saved offline — will retry when connected')
}
}
async function flushQueue() {
const queue = JSON.parse(localStorage.getItem('wc_queue') || '[]')
if (!queue.length) return
const now = Date.now()
for (const item of queue) {
const ageMinutes = (now - item.queuedAt) / 60000
if (ageMinutes > 15) {
// Mark as late — Ergani will require a justification reason
item.payload.late = true
item.payload.lateReason = 'EMPLOYER_SYSTEM_FAILURE'
}
await callErganiAPI(item.payload)
}
localStorage.removeItem('wc_queue')
}
window.addEventListener('online', flushQueue)
If more than 15 minutes have passed, the submission is considered εκπρόθεσμη. Accepted justification codes are EMPLOYER_SYSTEM_FAILURE, POWER_TELECOM_OUTAGE, and ERGANI_CONNECTIVITY. You must also notify the Labour Inspectorate (Επιθεώρηση Εργασίας) immediately when the outage occurs and again when it ends. See Work Card overview.
Where to go next
- Work Card Overview — field definitions, validation rules, CardScanner app
- Eligible Sectors — which businesses are required to use the Work Card
- API Overview — full REST API reference for
WRKCardSEand other endpoints