I Pointed Devin Desktop at the Webhook Docs and Had It Build a LINE Inbound Handler in 20 Minutes
Devin Desktop — the rebrand of Windsurf that shipped in early 2026 with the SWE-1.6 model pushing 950 tokens per second — is the AI coding tool most developers I know are reaching for right now. The pitch is simple: point it at a reference doc, describe what you want, and iterate until it runs. I wanted to see how far that workflow goes when the reference is a webhook API, not a framework guide.
The task: build a Python server that receives LINE messages as webhook events and logs them in a structured format — the kind of handler a small team would deploy behind a queue or a ticketing system. No LINE Official Account, no Messaging API credentials, no channel access token management. Just inbound messages arriving as normalized JSON.
What you end up with
A FastAPI server (about 40 lines) that:
- Accepts
message.receivedevents from UnifyPort’s unified webhook - Verifies every delivery with HMAC-SHA256 against your
signing_secret - Logs each message in structured JSON — provider, sender, text, timestamp
- Returns 200 for all events, 401 for invalid signatures
Time: about 20 minutes. You need a UnifyPort workspace with a LINE account connected (QR code scan from the LINE app) and Python 3.10+.
Why not the LINE Messaging API directly?
The official path:
- Create a LINE Official Account — requires a LINE Business ID, which requires a company or verified individual identity
- Enable the Messaging API in the LINE Developers console — configure a channel, generate a channel access token, set a webhook URL
- Parse LINE’s webhook payload: events arrive as
{"events": [{"type": "message", "message": {"type": "text", "text": "..."}, "source": {"userId": "U..."}}]}— a nested, LINE-specific structure - Verify the signature using the channel secret with
x-line-signature— LINE uses Base64-encoded HMAC-SHA256, not hex - Handle reply tokens: each event includes a
replyTokenvalid for 30 seconds — miss the window and the reply silently fails
The code works, but it is LINE-only. The webhook payload shape, the signature format, the reply-token mechanism — none of it transfers to WhatsApp or Telegram. Adding a second platform means a second integration from scratch.
UnifyPort’s unofficial interface connects a personal LINE account — QR scan, no Official Account required — and delivers every inbound message as a normalized message.received event. Same shape as WhatsApp, Telegram, TikTok, Zalo, and X. One handler covers all six.
Setup: feed Devin Desktop the API reference
Open Devin Desktop and start a new session. Before the first prompt, give it context:
- Paste the
message.receivedevent payload from the UnifyPort webhook docs:{ "event": "message.received", "account_id": "acct_7kQnWx", "provider": "line", "from": "user_a3f82c", "text": "Can I reschedule to Friday?", "timestamp": 1751270400, "message_id": "line_msg_5e9d21" } - Paste the HMAC-SHA256 verification section — the
x-unifyport-signatureheader, the hex digest computed over the raw request body using thesigning_secret
Devin indexes the pasted content as part of its session context. With SWE-1.6 Fast processing at 950 tok/s, the docs digest takes seconds. Cursor, Claude Code, and Copilot work the same way — paste or attach. The tool is interchangeable; the docs are what matters.
The build, prompt by prompt
First prompt — the handler skeleton:
Using the UnifyPort webhook reference I just pasted, write a FastAPI server with a POST /webhook route. When the event is “message.received”, log the provider, from, text, and timestamp as structured JSON using Python’s logging module. Return 200 for all events. Use uvicorn to serve it.
Devin reads the reference context and produces:
from fastapi import FastAPI, Request
import logging, json
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("inbound")
app = FastAPI()
@app.post("/webhook")
async def webhook(request: Request):
evt = await request.json()
if evt.get("event") == "message.received":
logger.info(json.dumps({
"provider": evt["provider"],
"from": evt["from"],
"text": evt["text"],
"timestamp": evt["timestamp"],
"message_id": evt["message_id"],
}))
return {"status": "ok"}
Twelve lines of handler. No LINE-specific imports, no linebot SDK, no channel access token. The event shape is flat enough that there is nothing to parse.
Second prompt — signature verification:
Add HMAC-SHA256 signature verification. The header is x-unifyport-signature, the secret comes from UNIFYPORT_SIGNING_SECRET in the environment, and the HMAC runs over the raw request body bytes — not re-serialized JSON. Return 401 if verification fails. Use a timing-safe comparison.
Devin adds the verification layer:
import hmac, hashlib, os
SIGNING_SECRET = os.environ["UNIFYPORT_SIGNING_SECRET"]
async def verify_signature(request: Request) -> bytes:
body = await request.body()
sig = request.headers.get("x-unifyport-signature", "")
expected = hmac.new(
SIGNING_SECRET.encode(), body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig, expected):
raise HTTPException(status_code=401, detail="invalid signature")
return body
It uses hmac.compare_digest for timing-safe comparison and operates on request.body() — the raw bytes — rather than re-serializing request.json(). The prompt specified “raw request body bytes,” and Devin handled it correctly without a correction round.
Third prompt — combine and clean up:
Merge the signature verification into the webhook handler. Parse the verified body as JSON after the check passes. Add a health check at GET /health. Include a uvicorn.run block for port 8000.
The complete server.py:
from fastapi import FastAPI, Request, HTTPException
import hmac, hashlib, os, json, logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("inbound")
SIGNING_SECRET = os.environ["UNIFYPORT_SIGNING_SECRET"]
app = FastAPI()
@app.get("/health")
async def health():
return {"status": "ok"}
@app.post("/webhook")
async def webhook(request: Request):
body = await request.body()
sig = request.headers.get("x-unifyport-signature", "")
expected = hmac.new(
SIGNING_SECRET.encode(), body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig, expected):
raise HTTPException(status_code=401, detail="invalid signature")
evt = json.loads(body)
if evt.get("event") == "message.received":
logger.info(json.dumps({
"provider": evt["provider"],
"from": evt["from"],
"text": evt["text"],
"timestamp": evt["timestamp"],
"message_id": evt["message_id"],
}))
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Under 40 lines. Signature-verified, structured logging, ready to deploy behind any reverse proxy.
Run it and watch a LINE message arrive
Connect a LINE account in your UnifyPort workspace — LINE supports QR code auth, so you open the LINE app, scan, and the account is linked in seconds. No LINE Official Account, no Business ID, no Messaging API channel setup.
Register a webhook endpoint pointed at your server with subscribed_events: ["message.received"] and a signing_secret. Start the server:
UNIFYPORT_SIGNING_SECRET=your_secret python server.py
Send a test message to your LINE account from a friend. The event arrives:
{
"event": "message.received",
"account_id": "acct_3Xk1wL",
"provider": "line",
"from": "user_a3f82c",
"text": "Can I reschedule to Friday?",
"timestamp": 1751270400,
"message_id": "line_msg_5e9d21"
}
Your server verifies the signature and logs the structured JSON. If the signature check fails — 401 — verify that UNIFYPORT_SIGNING_SECRET matches the value set on the webhook endpoint.
Add WhatsApp without changing a line
Connect a WhatsApp account in the same workspace, subscribe the same webhook endpoint. A WhatsApp message arrives in the same shape:
{
"event": "message.received",
"account_id": "acct_7kQnWx",
"provider": "whatsapp",
"from": "user_d4f29a",
"text": "Order confirmed, shipping Monday",
"timestamp": 1751270460,
"message_id": "wa_msg_8b3e71"
}
Same handler. Same structured log. The provider field switches from "line" to "whatsapp", but the code path is identical. A LINE Messaging API handler would need a completely separate WhatsApp integration — different webhook payload, different signature format, different SDK. This handler serves six platforms because the event shape does not change.
That is the workflow: paste the UnifyPort API reference into Devin Desktop, describe the handler, iterate until it runs. The tool writes the code; the normalized webhook makes the code work for every channel. Swap Devin for Cursor, Claude Code, or Copilot — the reference stays the same, and the handler stays under 40 lines.