Skip to content

Check-In API Integration

Check-In API Integration

This section combines the RFID tag reading, HTTP requests, and file-based state management into a complete check-in/check-out flow in Node-RED. Learning this section will enable you to:

  • Build a complete Node-RED flow for RFID-based check-in/check-out
  • Implement file-based state tracking to distinguish check-in from check-out
  • Handle the complete lifecycle: tag scan → file check → create/update → delete
  • Debug and test the complete flow end-to-end

Before starting this section, ensure you have:

  • ESP32 reading RFID tags and publishing to MQTT (05-04)
  • TimeTagger installed and API token ready (05-05, 05-06)
  • HTTP POST request tested (05-07)
  • Node-RED installed and MQTT configured
  • Node-RED FS (File System) node installed

The system uses a simple state tracking mechanism based on file existence on the Node-RED server:

┌─────────────────────┐
│ RFID Tag Detected │
│ (MQTT message) │
└──────────┬──────────┘
┌───────▼────────┐
│ Check File: │
│ timerecord.txt│
└───────┬────────┘
┌────────────┴────────────┐
│ │
┌────▼────┐ ┌────▼────┐
│ FILE NOT│ │ FILE │
│ EXISTS │ │ EXISTS │
└────┬────┘ └────┬────┘
│ │
┌────▼────┐ ┌────▼──────────┐
│ CHECK-IN│ │ CHECK-OUT │
│ │ │ │
│ Create │ │ Read file │
│ JSON │ │ Update t2 │
│ file │ │ POST to API │
│ with t1 │ │ Delete file │
└─────────┘ └────────────────┘

Key Logic:

  • Check-in (file does not exist): Create a JSON file with start timestamp
  • Check-out (file exists): Read file, add end timestamp, POST to TimeTagger, delete file

Why use a flat file instead of an in-memory variable?

ReasonExplanation
PersistenceSurvives Node-RED restarts
SimplicityNo database required
DebuggableEasily inspect file contents
Single-userSuitable for demo/PoC scenarios

File Location: /data/timerecord.txt inside the Node-RED container

File Format (JSON):

[
{
"key": "1715942400a1b2c3d4e5f6",
"t1": 1715942400,
"t2": 0,
"ds": "ESP32_TEAM - check-in",
"mt": 1715942400,
"st": 0
}
]
Terminal window
# In Node-RED container or terminal
npm install node-red-contrib-fs-ops
# Or via Node-RED Palette Manager:
# Search for "node-red-contrib-fs-ops" and install

Alternatively, use the built-in File System node (if available) or install:

Manage Palette → Install → search "node-red-contrib-fs"

Below is the full Node-RED flow for the check-in/check-out system:

Flow Structure:
[MQTT In: RFID Tag]
[Function: Build Record Object]
[FS Access: Check if file exists]
├──[OUTPUT 1: File EXISTS → Check-OUT]──→[Function: Process Check-Out]
│ │
│ ▼
│ [FS Read: Read file]
│ │
│ ▼
│ [Function: Add End Time]
│ │
│ ▼
│ [HTTP Request: PUT to API]
│ │
│ ▼
│ [FS Remove: Delete file]
└──[OUTPUT 2: File NOT exists → Check-IN]──→[Function: Process Check-In]
[FS Write: Create file]
Node: MQTT In (RFID Tag)
┌──────────────────────────────────┐
│ Server: localhost:1883 │
│ Topic: asset/tracking/tag │
│ QoS: 1 │
│ Output: a parsed JSON object │
└──────────────────────────────────┘

ESP32 publishes:

{
"uid": "04A3B2C1",
"name": "ESP32_TEAM"
}
// Function Node: "Build Record Object"
// Creates the initial record object from RFID tag data
const authToken = global.get("timetaggerAuth")?.token || "YOUR_TOKEN_HERE";
const baseUrl = global.get("timetaggerAuth")?.baseUrl || "http://timetagger:80/api/v2";
// Extract tag info from MQTT message
const uid = msg.payload.uid || "UNKNOWN";
const tagName = msg.payload.name || uid;
// Generate unique key
const now = Math.floor(Date.now() / 1000);
const key = now.toString(16) + Math.random().toString(36).substring(2, 10);
// Store record data for later use
msg.record = {
key: key,
t1: now,
t2: 0, // Will be set on check-out
ds: tagName + " - " + uid,
mt: now,
st: 0
};
// Store auth info
msg.authToken = authToken;
msg.baseUrl = baseUrl;
// Store file path
msg.filePath = "/data/timerecord.txt";
msg.fileName = "timerecord.txt";
return msg;

Step 5: FS Access Node (Check File Existence)

Section titled “Step 5: FS Access Node (Check File Existence)”
Node: FS Access (Check File)
┌──────────────────────────────────┐
│ Operation: Access │
│ Filename: msg.filePath │
│ Type: msg.fileName │
│ Output 1: File accessible │
│ Output 2: File NOT accessible │
└──────────────────────────────────┘

How it works:

  • Output 1 (accessible): File exists → This is a CHECK-OUT
  • Output 2 (not accessible): File does not exist → This is a CHECK-IN
// Function Node: "Process Check-In"
// Creates a new JSON file with the initial record
// Get the record data
const record = msg.record;
const filePath = msg.filePath;
// The file content should be a JSON array
const fileContent = JSON.stringify([record]);
// Set up the write operation
msg.filename = "timerecord.txt"; // Simple filename (relative to Node-RED data dir)
msg.filedata = fileContent;
// Also publish confirmation to MQTT for LED feedback
const confirmMsg = {
topic: "asset/tracking/feedback",
payload: JSON.stringify({
action: "CHECK_IN",
name: record.ds,
key: record.key,
t1: record.t1
})
};
// Send to both outputs
return [[msg], [confirmMsg]];

FS Write Node:

Node: FS Write (Write File)
┌──────────────────────────────────┐
│ Operation: Write to file │
│ Filename: msg.filename │
│ Data: msg.filedata │
│ Encoding: utf8 │
│ Action: overwrite file │
│ Create if missing: yes │
└──────────────────────────────────┘
// Function Node: "Process Check-Out"
// Reads existing file, updates with end time, sends to API
const record = msg.record;
const authToken = msg.authToken;
const baseUrl = msg.baseUrl;
const now = Math.floor(Date.now() / 1000);
// The FS Read node will put file content in msg.payload
// We need to parse it and update the end time
// Store the auth info for later nodes
msg.authToken = authToken;
msg.baseUrl = baseUrl;
// Store for the next step
msg.currentTime = now;
return msg;

FS Read Node:

Node: FS Read (Read File)
┌──────────────────────────────────┐
│ Operation: Read file │
│ Filename: msg.filename │
│ Encoding: utf8 │
│ Output: msg.payload │
└──────────────────────────────────┘
// Function Node: "Add End Time and Send"
// Updates the record with end time and prepares API request
const authToken = msg.authToken;
const baseUrl = msg.baseUrl;
const now = msg.currentTime;
// Parse the stored record
let records;
try {
records = JSON.parse(msg.payload);
} catch (e) {
node.error("Failed to parse stored record: " + e.toString());
msg.status = { fill: "red", shape: "dot", text: "parse error" };
return null;
}
if (!Array.isArray(records) || records.length === 0) {
node.error("Invalid records array in file");
return null;
}
// Update the record with end time
const record = records[0];
record.t2 = now; // Set end time to now
record.mt = now; // Update modified time
// Prepare HTTP request
msg.headers = {
"Authorization": "Bearer " + authToken,
"Content-Type": "application/json"
};
msg.method = "PUT";
msg.url = baseUrl + "/records";
msg.payload = [record]; // Array wrapper
// Also prepare MQTT feedback for LED
msg.feedback = {
topic: "asset/tracking/feedback",
payload: JSON.stringify({
action: "CHECK_OUT",
name: record.ds,
key: record.key,
t1: record.t1,
t2: record.t2,
duration: record.t2 - record.t1
})
};
return msg;

After successfully posting to the API, delete the file:

Node: FS Remove (Delete File)
┌──────────────────────────────────┐
│ Operation: Delete file │
│ Filename: msg.filename │
│ Type: string │
└──────────────────────────────────┘

Flow completion: After file deletion, send a confirmation MQTT message to the ESP32 for LED feedback.

Import this complete flow into Node-RED:

[
{
"id": "mqtt-in-tag",
"type": "mqtt in",
"name": "RFID Tag",
"topic": "asset/tracking/tag",
"qos": "1",
"broker": "localhost",
"wires": [["build-record"]]
},
{
"id": "build-record",
"type": "function",
"name": "Build Record",
"func": "const authToken = global.get(\"timetaggerAuth\")?.token || \"YOUR_TOKEN\";\nconst baseUrl = global.get(\"timetaggerAuth\")?.baseUrl || \"http://timetagger:80/api/v2\";\nconst uid = msg.payload.uid || \"UNKNOWN\";\nconst tagName = msg.payload.name || uid;\nconst now = Math.floor(Date.now() / 1000);\nconst key = now.toString(16) + Math.random().toString(36).substring(2, 10);\nmsg.record = { key: key, t1: now, t2: 0, ds: tagName, mt: now, st: 0 };\nmsg.authToken = authToken;\nmsg.baseUrl = baseUrl;\nmsg.filename = \"timerecord.txt\";\nreturn msg;",
"wires": [["fs-access-check"]]
},
{
"id": "fs-access-check",
"type": "fs-access",
"name": "Check File",
"path": "/data/timerecord.txt",
"wire": false,
"wires": [["check-out-flow"], ["check-in-flow"]]
},
{
"id": "check-out-flow",
"type": "function",
"name": "Check-Out",
"func": "msg.authToken = msg.authToken;\nmsg.baseUrl = msg.baseUrl;\nmsg.currentTime = Math.floor(Date.now() / 1000);\nmsg.filename = \"timerecord.txt\";\nreturn msg;",
"wires": [["fs-read-file"]]
},
{
"id": "fs-read-file",
"type": "fs-read",
"name": "Read File",
"filename": "timerecord.txt",
"format": "utf8",
"wires": [["update-and-send"]]
},
{
"id": "update-and-send",
"type": "function",
"name": "Update & POST",
"func": "const authToken = msg.authToken;\nconst baseUrl = msg.baseUrl;\nconst now = msg.currentTime;\nlet records;\ntry { records = JSON.parse(msg.payload); } catch(e) { return null; }\nif (!Array.isArray(records) || records.length === 0) return null;\nrecords[0].t2 = now;\nrecords[0].mt = now;\nmsg.headers = { \"Authorization\": \"Bearer \" + authToken, \"Content-Type\": \"application/json\" };\nmsg.method = \"PUT\";\nmsg.url = baseUrl + \"/records\";\nmsg.payload = [records[0]];\nreturn msg;",
"wires": [["http-put-api", "mqtt-feedback-out"]]
},
{
"id": "http-put-api",
"type": "http request",
"name": "PUT to TimeTagger",
"method": "PUT",
"ret": "txt",
"url": "",
"tls": "",
"wires": [["fs-remove-file"]]
},
{
"id": "fs-remove-file",
"type": "fs-remove",
"name": "Delete File",
"path": "timerecord.txt",
"wires": [["debug-result"]]
},
{
"id": "check-in-flow",
"type": "function",
"name": "Check-In",
"func": "const record = msg.record;\nconst fileContent = JSON.stringify([record]);\nmsg.filedata = fileContent;\nmsg.filename = \"timerecord.txt\";\nreturn msg;",
"wires": [["fs-write-file", "mqtt-feedback-in"]]
},
{
"id": "fs-write-file",
"type": "fs-write",
"name": "Write File",
"filename": "timerecord.txt",
"format": "utf8",
"wires": []
},
{
"id": "mqtt-feedback-in",
"type": "mqtt out",
"name": "Feedback IN",
"topic": "asset/tracking/feedback",
"qos": "1",
"broker": "localhost",
"wires": []
},
{
"id": "mqtt-feedback-out",
"type": "mqtt out",
"name": "Feedback OUT",
"topic": "asset/tracking/feedback",
"qos": "1",
"broker": "localhost",
"wires": []
},
{
"id": "debug-result",
"type": "debug",
"name": "Result",
"active": true,
"wires": []
}
]

Test the complete flow:

Terminal window
# 1. Simulate a CHECK-IN:
mosquitto_pub -t "asset/tracking/tag" \
-m '{"uid":"04A3B2C1","name":"ESP32_TEAM"}'
# Expected:
# - File created: /data/timerecord.txt
# - MQTT feedback: {"action":"CHECK_IN","name":"ESP32_TEAM",...}
# 2. Verify file exists:
docker exec -it nodered cat /data/timerecord.txt
# Expected: [{"key":"...","t1":...,"t2":0,"ds":"ESP32_TEAM",...}]
# 3. Simulate a CHECK-OUT (publish same tag again):
mosquitto_pub -t "asset/tracking/tag" \
-m '{"uid":"04A3B2C1","name":"ESP32_TEAM"}'
# Expected:
# - File read and deleted
# - PUT request to TimeTagger API
# - New record appears in TimeTagger
# - MQTT feedback: {"action":"CHECK_OUT",...}

Verification Checklist:

  • First tag scan creates file (check-in)
  • Second tag scan sends record to API (check-out)
  • File is deleted after check-out
  • Record appears correctly in TimeTagger
  • MQTT feedback messages are published
  • System resets and is ready for next check-in

Symptom: FS Access node always goes to “file exists” path

Cause: File path incorrect or permissions issue

Solution:

Terminal window
# Check Node-RED data directory
docker exec -it nodered ls -la /data/
# Ensure writable
docker exec -it nodered touch /data/test.txt

Symptom: PUT request fails or returns error

Cause: Record key changed between check-in and check-out

Solution: Store the exact record object and use it unchanged

Symptom: If file exists but user checks in again

Solution: Handle edge case in the flow:

if (msg.action === "CHECK_IN" && fileExists) {
node.warn("User already checked in!");
// Publish error feedback
}
  • Recommended: Always verify the API response before deleting the file
  • Recommended: Add error handling for file read/write failures
  • Recommended: Use unique file names per user if multi-user support is needed
  • Avoid: Hardcoding file paths — use Node-RED environment variables
  • Avoid: Deleting the file before confirming API success
  1. State tracking uses a simple JSON flat file on the server
  2. Check-in: File does not exist → create file with start timestamp
  3. Check-out: File exists → read file, add end time, POST to API, delete file
  4. The complete flow integrates MQTT, File System, and HTTP nodes in Node-RED
  5. MQTT feedback informs the ESP32 to light appropriate LEDs

Writing Date: 2026-05-17
Based on Source File: 校正版/10 Time recording witht RFID und TimeTagger.md
Target Audience: Alibaba.com IoT Pre-sales Engineer
Status: ✅ Completed