From Internal Tool to API Product: The PunchConnect Story
How we turned an internal biometric device integration built for 24,000 employees into a cloud REST API product β and what we learned along the way.
Introduction
PunchConnect wasn't supposed to be a product. It was built out of frustration β and 4 months of production hell.
We were building AgriWise, a workforce management platform for agricultural companies in Morocco. 24,000+ employees across dozens of sites. Every site had ZKTeco biometric devices. And we needed to get attendance data from those devices into our cloud application in real time.
Nothing on the market worked. So we built our own. This is the story of how that internal tool became a standalone API product β and the engineering decisions that made it possible.
The Problem That Started Everything
AgriWise had a straightforward requirement: get punch data from biometric devices into a cloud application, reliably, in real time.
Sounds simple. It's not.
<svg viewBox="0 0 700 300" xmlns="http://www.w3.org/2000/svg" style="max-width:700px;width:100%;font-family:system-ui,sans-serif">
<rect width="700" height="300" fill="#1a1a2e" rx="12"/>
<text x="350" y="30" fill="#e0e0e0" font-size="17" font-weight="bold" text-anchor="middle">What We Tried (and Why It Failed)</text>
<!-- Attempt 1 -->
<rect x="30" y="55" width="195" height="110" rx="8" fill="#3e1e1e" stroke="#ef5350" stroke-width="1"/>
<text x="127" y="80" fill="#ef5350" font-size="15" font-weight="bold" text-anchor="middle">Open-Source SDKs</text>
<text x="127" y="105" fill="#ccc" font-size="14" text-anchor="middle">pyzk, node-zklib</text>
<text x="127" y="125" fill="#ccc" font-size="14" text-anchor="middle">LAN-only, no cloud</text>
<text x="127" y="145" fill="#ef5350" font-size="14" text-anchor="middle">β Failed at 10 devices</text>
<!-- Attempt 2 -->
<rect x="252" y="55" width="195" height="110" rx="8" fill="#3e1e1e" stroke="#ef5350" stroke-width="1"/>
<text x="350" y="80" fill="#ef5350" font-size="15" font-weight="bold" text-anchor="middle">Vendor Software</text>
<text x="350" y="105" fill="#ccc" font-size="14" text-anchor="middle">ZKBioAccess, ADMS</text>
<text x="350" y="125" fill="#ccc" font-size="14" text-anchor="middle">Windows-only, no API</text>
<text x="350" y="145" fill="#ef5350" font-size="14" text-anchor="middle">β No cloud integration</text>
<!-- Attempt 3 -->
<rect x="475" y="55" width="195" height="110" rx="8" fill="#3e1e1e" stroke="#ef5350" stroke-width="1"/>
<text x="572" y="80" fill="#ef5350" font-size="15" font-weight="bold" text-anchor="middle">Competitors</text>
<text x="572" y="105" fill="#ccc" font-size="14" text-anchor="middle">CAMS, eSSL, MinopCloud</text>
<text x="572" y="125" fill="#ccc" font-size="14" text-anchor="middle">Opaque pricing, poor docs</text>
<text x="572" y="145" fill="#ef5350" font-size="14" text-anchor="middle">β Not developer-friendly</text>
<!-- Solution -->
<rect x="30" y="195" width="640" height="85" rx="8" fill="#1b3a2d" stroke="#81c784" stroke-width="2"/>
<text x="350" y="220" fill="#81c784" font-size="17" font-weight="bold" text-anchor="middle">So We Built It Ourselves</text>
<text x="350" y="245" fill="#ccc" font-size="14" text-anchor="middle">4 months of engineering β protocol engine β battle-tested with 24,000+ employees</text>
<text x="350" y="267" fill="#ccc" font-size="14" text-anchor="middle">Then we realized: if we had this problem, so does everyone else</text>
</svg>
We tried every option. Open-source SDKs worked for a single device on a LAN. Vendor software was Windows-only with no API. Competitors in this space had opaque pricing, poor documentation, and architectures that assumed on-premise deployment.
So we spent 4 months building a protocol engine from scratch. It had to handle:
- 80+ devices across sites with varying connectivity (fiber to 3G cellular)
- Automatic reconnection and data sync after network outages
- Zero data loss β every punch had to arrive, eventually
- Real-time delivery β attendance data needed to flow within seconds, not minutes
From One Customer to a Platform
After running in production for 6 months with AgriWise, we kept hearing the same question from other companies: "How do you connect ZKTeco devices to the cloud?"
Developers on forums, Stack Overflow, GitHub issues β all hitting the same wall. The device protocol was undocumented. The open-source libraries were unmaintained. Cloud connectivity was an unsolved problem.
We didn't build a demo. We opened our production system. The same engine that handles AgriWise's 24,000+ employees now powers every PunchConnect API call.
<svg viewBox="0 0 700 250" xmlns="http://www.w3.org/2000/svg" style="max-width:700px;width:100%;font-family:system-ui,sans-serif">
<rect width="700" height="250" fill="#1a1a2e" rx="12"/>
<text x="350" y="30" fill="#e0e0e0" font-size="17" font-weight="bold" text-anchor="middle">Internal Tool β API Product: What Changed</text>
<!-- Internal -->
<rect x="30" y="55" width="300" height="170" rx="8" fill="#2d2d44" stroke="#444" stroke-width="1"/>
<text x="180" y="80" fill="#4fc3f7" font-size="15" font-weight="bold" text-anchor="middle">Internal Tool (AgriWise)</text>
<text x="180" y="105" fill="#ccc" font-size="14" text-anchor="middle">β’ Single tenant</text>
<text x="180" y="125" fill="#ccc" font-size="14" text-anchor="middle">β’ Hardcoded config</text>
<text x="180" y="145" fill="#ccc" font-size="14" text-anchor="middle">β’ Direct database access</text>
<text x="180" y="165" fill="#ccc" font-size="14" text-anchor="middle">β’ Internal monitoring</text>
<text x="180" y="185" fill="#ccc" font-size="14" text-anchor="middle">β’ No docs needed</text>
<text x="180" y="210" fill="#81c784" font-size="14" text-anchor="middle">Worked for us β</text>
<!-- Arrow -->
<text x="350" y="145" fill="#ff9800" font-size="34" text-anchor="middle">β</text>
<!-- Product -->
<rect x="370" y="55" width="300" height="170" rx="8" fill="#1a3a5c" stroke="#4fc3f7" stroke-width="2"/>
<text x="520" y="80" fill="#4fc3f7" font-size="15" font-weight="bold" text-anchor="middle">API Product (PunchConnect)</text>
<text x="520" y="105" fill="#ccc" font-size="14" text-anchor="middle">β’ Multi-tenant isolation</text>
<text x="520" y="125" fill="#ccc" font-size="14" text-anchor="middle">β’ API key management</text>
<text x="520" y="145" fill="#ccc" font-size="14" text-anchor="middle">β’ REST API + Webhooks</text>
<text x="520" y="165" fill="#ccc" font-size="14" text-anchor="middle">β’ Per-device rate limiting</text>
<text x="520" y="185" fill="#ccc" font-size="14" text-anchor="middle">β’ Full documentation</text>
<text x="520" y="210" fill="#81c784" font-size="14" text-anchor="middle">Works for everyone β</text>
</svg>
The Engineering Decisions That Mattered
Turning an internal tool into an API product isn't just adding authentication. It's rethinking the entire architecture. Here's what we had to build:
1. Multi-Tenancy with Device Isolation
Every customer's devices are completely isolated. Customer A can never see Customer B's data, devices, or webhooks β even if they share the same physical server infrastructure.
# Every API request is scoped to the authenticated tenant@app.route("/v1/devices", methods=["GET"])@require_api_keydef list_devices():tenant_id = request.auth.tenant_id # From API keydevices = db.query("SELECT * FROM devices WHERE tenant_id = %s",[tenant_id])return jsonify({"devices": devices})
2. API Design: Lessons from Stripe
We studied Stripe, Twilio, and GitHub's APIs obsessively. The patterns that made them great:
- Consistent response format β every endpoint returns the same structure
- Clear error codes β not just HTTP status codes, but specific error identifiers
- Idempotency keys β retry-safe operations on every mutating endpoint
- Pagination β cursor-based, not offset-based (stable with real-time data)
# Consistent error format across all endpointscurl -X GET https://api.punchconnect.com/v1/devices/nonexistent \-H "Authorization: Bearer YOUR_API_KEY"# Response:# {# "error": {# "code": "device_not_found",# "message": "No device found with ID 'nonexistent'",# "status": 404,# "doc_url": "https://docs.punchconnect.com/errors/device_not_found"# }# }
3. Webhook Delivery System
The internal tool sent data directly to our database. An API product needs to send data to thousands of different endpoints, each with different reliability characteristics.
# Webhook delivery with exponential backoffRETRY_DELAYS = [10, 60, 300, 1800, 7200] # secondsdef deliver_webhook(webhook_url: str, payload: dict, secret: str):"""Deliver webhook with signature and retries."""body = json.dumps(payload)signature = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()for attempt, delay in enumerate(RETRY_DELAYS):try:resp = requests.post(webhook_url,data=body,headers={"Content-Type": "application/json","X-PunchConnect-Signature": signature,"X-PunchConnect-Delivery": str(uuid4()),},timeout=10)if resp.status_code < 300:return True # Deliveredexcept requests.RequestException:passtime.sleep(delay)# All retries exhausted β mark as failedlog_delivery_failure(webhook_url, payload)return False
4. Rate Limiting Per Device
When an internal tool, you control the load. With an API product, you need to protect against abuse β both accidental and intentional.
# Rate limit headers on every response# X-RateLimit-Limit: 100# X-RateLimit-Remaining: 87# X-RateLimit-Reset: 1711594800curl -X GET https://api.punchconnect.com/v1/devices \-H "Authorization: Bearer YOUR_API_KEY"
5. Documentation as Product
For an internal tool, documentation is a wiki page nobody reads. For an API product, documentation is the product. If developers can't figure out your API in 5 minutes, they'll leave.
We built:
- Interactive API reference with try-it-now examples
- Quick-start guide (device to first webhook in under 5 minutes)
- Code samples in Python, JavaScript, cURL, PHP, Ruby
- Error code reference with solutions
- Webhook payload reference with full JSON schemas
What We Learned
After 18 months running both AgriWise (internal) and PunchConnect (product) on the same engine:
| Metric | Internal (AgriWise) | Product (PunchConnect) |
|--------|-------------------|----------------------|
| Devices | 80+ | Growing |
| Employees | 24,000+ | Varies per customer |
| Uptime | 99.7% | 99.7% (same engine) |
| Webhook latency | 340ms avg | 340ms avg |
| Lost records | Zero | Zero |
| Time to first integration | 4 months | Under 5 minutes |
The last row is the product. We spent 4 months integrating. Our customers spend 5 minutes. That's the value of an API product β you package your hard-won engineering into a simple interface.
The Lessons for Other Internal-to-Product Journeys
If you're sitting on an internal tool that solves a real problem, here's what we wish we'd known:
1. Don't build a demo β open your production system. Customers trust battle-tested infrastructure over polished landing pages.
2. Study the best APIs obsessively. Stripe's consistency, Twilio's error messages, GitHub's pagination. Copy the patterns, not the code.
3. Documentation is not an afterthought. Budget 30% of engineering time for docs. It's the first thing developers evaluate.
4. Multi-tenancy is harder than you think. Every query, every log, every webhook needs tenant scoping. Bolt this on from day one.
5. Price transparently. Our competitors hide pricing behind "contact sales." We put ours on the website: $200/device. Developers appreciate knowing the cost before they evaluate the product.
What's Next
PunchConnect is now serving customers across MENA, Sub-Saharan Africa, and Southeast Asia. The roadmap:
- Additional device protocols β beyond ZKTeco (Suprema, HID, ZKBioAccess)
- Device provisioning API β register and configure devices programmatically
- Real-time streaming β WebSocket connections for live attendance feeds
- Marketplace β pre-built integrations for popular ERP/HRMS platforms
We're building the Stripe of biometric device integration. The infrastructure layer that lets developers forget about device protocols and focus on their product.
FAQ
Is PunchConnect still used by AgriWise internally?
Yes. AgriWise runs on the exact same PunchConnect engine. Every improvement we make for external customers benefits AgriWise too β and vice versa. The production battle-testing never stops.
How long does it take to integrate PunchConnect?
Most developers get their first webhook within 5 minutes: register a device, configure the device in the dashboard, and start receiving real-time attendance data via your API endpoint.
What devices does PunchConnect support?
Currently, PunchConnect supports ZKTeco biometric devices β the most widely deployed brand globally. This includes SpeedFace, ProFace, UA860, K40, MB460, and most models with network connectivity. Support for additional brands is on the roadmap.
How is PunchConnect different from open-source libraries like pyzk?
Open-source libraries like pyzk connect to devices over LAN β one device at a time, on the same network. PunchConnect is a cloud service: devices connect outbound, work across any network, handle offline buffering, and deliver data via webhooks. It's the difference between a library and a platform.
Can I try PunchConnect before committing?
Yes. 7-day free trial with full API access, no credit card required. Connect a device and see it working before you decide.
---
Every API product starts as someone's internal problem. We spent 4 months solving biometric device connectivity for AgriWise. Now that solution is available to every developer who faces the same challenge.
Start your free 7-day trial β from zero to first webhook in under 5 minutes.