A minimal mobile embeddable web tool. The user uploads a CSV or Excel file; it is parsed to JSON entirely in the browser and POSTed directly to a webhook URL supplied via a query parameter. No file ever touches a server in the default flow.
npm install
npm run devOpen:
http://localhost:3000/?webhook=https://webhook.site/<your-id>
Upload a .csv, .xls, or .xlsx file and watch the JSON arrive at your webhook.
| Param | Required | Description |
|---|---|---|
webhook |
Yes | URL-encoded http(s) endpoint that receives the POST. |
token |
No | Arbitrary string echoed back in the payload for lightweight auth. |
source |
No | Arbitrary label echoed back in the payload (e.g. mobile, kiosk). |
Example:
https://yourtool.com/?webhook=https%3A%2F%2Fhooks.example.com%2Fflow&token=abc123&source=mobile
Remember to URL-encode the webhook value: encodeURIComponent("https://hooks.example.com/flow").
The tool sends a single JSON POST with Content-Type: application/json:
{
"filename": "contacts.csv",
"rowCount": 2,
"source": "mobile",
"token": "abc123",
"receivedAt": "2026-06-10T06:40:00.000Z",
"data": [
{ "name": "Ada", "email": "ada@example.com" },
{ "name": "Linus", "email": "linus@example.com" }
]
}data is an array of row objects keyed by the file's header row. CSV values are type-coerced (numbers/booleans) where possible; Excel uses the first sheet.
<iframe
src="https://yourtool.com/?webhook=https%3A%2F%2Fhooks.example.com%2Fflow&token=abc123"
style="width: 100%; height: 360px; border: none;"
allow="clipboard-write"
></iframe>import { WebView } from "react-native-webview";
const webhook = encodeURIComponent("https://hooks.example.com/flow");
<WebView
source={{ uri: `https://yourtool.com/?webhook=${webhook}&source=app` }}
originWhitelist={["*"]}
/>;The embedded tool emits postMessage updates to its parent on every state change:
window.addEventListener("message", (e) => {
if (e.data?.type === "csv-to-json-webhook") {
// { phase, fileName, rowCount, message }
console.log(e.data.phase);
}
});The browser POSTs directly to your webhook from a different origin, so the webhook must return:
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type
Compatibility:
- Your own server / n8n webhook node — works (configure CORS).
- Zapier / Make (Integromat) — browser POSTs are typically blocked by CORS; use the proxy below.
If the target webhook can't send CORS headers, route through the included server-side proxy at app/api/convert/route.ts. It forwards the payload from the server, where CORS does not apply.
To enable it, change the fetch target in app/embed/EmbedTool.tsx from webhook to:
/api/convert?webhook=${encodeURIComponent(webhook)}
Before exposing publicly, lock the proxy down: add ALLOWED_WEBHOOK_HOSTS and/or a shared-secret check so it can't be abused as an open relay (SSRF).
- The webhook URL is visible in the address bar and browser history. Anyone with the link can POST to it. Use the
tokenparam (validated on your webhook) or HMAC signing for sensitive flows. - Parsing is capped at 25 MB to protect the browser.
Deploy to Vercel (or any Node host):
npm run build
npm startnext.config.ts sets Content-Security-Policy: frame-ancestors * so the page can be embedded anywhere. Restrict that to your domains in production.