Install
Install Form Capture
Create a site and key, post from your server, add visitor context, and verify the first submission — by hand or with an AI agent.
Install with an AI agent
Using Copilot, Cursor, Claude Code or Windsurf? Copy the prompt below into the agent (the key is read from your environment, so nothing to replace) and let it wire your form. Then run through Verify.
Add Buckhy Form Capture to this project so my form submissions are stored and emailed.
FACTS
- Endpoint: POST https://api.buckhy.com/v1/capture (JSON in, JSON out).
- Auth header: X-Api-Key. The key is SECRET and lives ONLY on the server, in the
environment variable BUCKHY_CAPTURE_KEY. Never NEXT_PUBLIC_*, VITE_*, or any
client bundle.
- Browser requests to api.buckhy.com are refused by CORS by design. The browser
posts to MY endpoint; my endpoint forwards to Buckhy.
- Request body: { form: "<label>", page: "<path>", fields: { "<name>": "<string>" },
context?: { ip, userAgent, referrer, landingPage, language, timezone, screen, utm } }
- Limits: 20 fields; names A-Z a-z 0-9 _ - (40 chars); values 2,000 chars. Flat strings only.
- Responses: 201 { success, id } stored · 200 { …, duplicate: true } idempotent replay
· 400 fields missing · 401 key missing/invalid · 403 wrong product or disabled site
· 409 Idempotency-Key reused with different content · 429 rate limit (40 per 15 min).
- Idempotency-Key header: 8-100 chars (a UUID). Create it ONCE per submission attempt
in the browser, pass it through my endpoint unchanged, reuse it on retry.
TASKS
1. Add a server endpoint POST /api/contact using this project's idiom (Next.js route
handler, Express route, serverless function, …) that:
- reads { fields, page } from the JSON body and drops my honeypot field ("website");
- forwards to https://api.buckhy.com/v1/capture with headers Content-Type:
application/json, X-Api-Key: process.env.BUCKHY_CAPTURE_KEY, and the incoming
Idempotency-Key (or crypto.randomUUID() if absent);
- adds context from the incoming request: ip (first value of x-forwarded-for),
userAgent, referrer (Referer header), language (first Accept-Language tag);
- returns Buckhy's status code and JSON body to the browser unchanged.
2. Wire my existing form(s) to POST JSON { fields, page: location.pathname } to
/api/contact with an Idempotency-Key created once per submission and reused on
retry. Show success and error text inline; never reload the page.
3. Add BUCKHY_CAPTURE_KEY= to .env.example and read the real value from .env locally.
Do not commit the real key.
RULES
- Never call api.buckhy.com or reference the key from client code.
- Do not invent fields the form does not have; send what the user typed, as strings.
- Keep my existing markup and styling; only change the submit behaviour.
VERIFY
- Start the dev server and submit the form once: /api/contact returns 201 with an id,
the record appears in Businesses -> my website -> Inbox. Check notification
delivery separately; an accepted API request does not prove email arrival.
- Submit the same content twice with the same Idempotency-Key: the second response
has duplicate: true and the console shows ONE record.
- Report every file you changed and anything you could not verify.
A good run adds one server endpoint, changes only your form's submit behaviour, keeps the key in .env, and reports a 201 from a real submission. Prefer to do it yourself? Continue below.
Where the call belongs
Form Capture is called server to server. Two reasons:
- The key is a secret. Anyone holding it can write into your inbox and consume your rate limit.
api.buckhy.comonly accepts browser requests from Buckhy's own applications. A request from your domain fails the CORS preflight with403 CORS_ORIGIN_DENIEDbefore it reaches the API.
So the shape is always browser → your endpoint → Buckhy. Your endpoint can be a framework route handler, an Express route, a serverless function, or a server-side form action.
Want forms with zero backend code? Managed Forms run in the browser with a publishable key and an origin lock.
1. Create a site and key
- In the console, open Businesses and add or select your website. Complete domain ownership verification.
- Select Test or Live, then open Apps > Form Capture > Configure. Set the form ID, required fields and notification email.
- In Integrate, choose your framework and create the credential. Its value is displayed once and stored only as a hash.
- Put it in your server's secret store:
BUCKHY_CAPTURE_KEY=bk_live_…
Complete the request below, run the app's Test checks and Activate. Later credential changes belong in API > Credentials; see safe replacement.
2. Post from your server
Send form, page, the visitor's fields, and — optionally — a context block describing the visitor. Full field rules are in the API reference.
Next.js (App Router)
// app/api/contact/route.ts
import { headers } from "next/headers";
export async function POST(request: Request) {
const { fields, page } = await request.json();
const h = await headers();
const upstream = await fetch("https://api.buckhy.com/v1/capture", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.BUCKHY_CAPTURE_KEY!,
// Reuse the same key when the browser retries the same submission.
"Idempotency-Key": request.headers.get("idempotency-key") ?? crypto.randomUUID(),
},
body: JSON.stringify({
form: "contact",
page,
fields,
context: {
ip: h.get("x-forwarded-for")?.split(",")[0]?.trim(),
userAgent: h.get("user-agent"),
referrer: h.get("referer"),
language: h.get("accept-language")?.split(",")[0],
},
}),
});
return Response.json(await upstream.json(), { status: upstream.status });
}
Express
app.post("/api/contact", express.json(), async (req, res) => {
const upstream = await fetch("https://api.buckhy.com/v1/capture", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.BUCKHY_CAPTURE_KEY,
"Idempotency-Key": req.get("Idempotency-Key") || crypto.randomUUID(),
},
body: JSON.stringify({
form: "contact",
page: req.body.page,
fields: req.body.fields,
context: { ip: req.ip, userAgent: req.get("user-agent"), referrer: req.get("referer") },
}),
});
res.status(upstream.status).json(await upstream.json());
});
Serverless function (any provider)
export default async function handler(event) {
const { fields, page } = JSON.parse(event.body || "{}");
const upstream = await fetch("https://api.buckhy.com/v1/capture", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Api-Key": process.env.BUCKHY_CAPTURE_KEY },
body: JSON.stringify({ form: "contact", page, fields }),
});
return { statusCode: upstream.status, body: await upstream.text() };
}
Browser side
Post to your own endpoint as you always have. Generate one idempotency key per submission attempt and reuse it if you retry:
const idempotencyKey = crypto.randomUUID(); // once per submission, reused on retry
async function submit(fields) {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": idempotencyKey },
body: JSON.stringify({ fields, page: location.pathname }),
});
if (!res.ok) throw new Error("Could not send — please try again.");
}
3. Send visitor context (optional)
The context object lets the console and alert emails show who submitted and from where. Every property is optional and validated; anything malformed is dropped silently.
| Property | Format | Used for |
|---|---|---|
ip |
IPv4 or IPv6 | Country lookup. The address itself is deleted after 30 days; the derived country is kept. |
userAgent |
string ≤ 500 | Device summary, e.g. "Safari on iPhone". |
referrer |
http(s)://… ≤ 500 |
Traffic source. |
landingPage |
path such as /events/naan-yaar |
First page of the visit. |
language |
BCP 47 tag, e.g. en-IN |
Visitor language. |
timezone |
IANA zone, e.g. Asia/Kolkata |
Visitor time zone. |
screen |
1170x2532 |
Screen size. |
utm |
object with source, medium, campaign, term, content (each ≤ 120) |
Campaign attribution. |
Because the request is authenticated with your key, Buckhy trusts the ip and userAgent you assert. Read them from the incoming request on your server; do not let the browser choose them.
4. Verify
Send one request from your terminal:
curl -X POST https://api.buckhy.com/v1/capture \
-H "Content-Type: application/json" \
-H "X-Api-Key: $BUCKHY_CAPTURE_KEY" \
-d '{"form":"contact","page":"/verify","fields":{"name":"Test","email":"you@example.com"}}'
Expected:
{ "success": true, "id": "6a9829095ce0e4f33a6536c8" }
Check the website's Inbox > Leads, the notification outcome, and your endpoint returning Buckhy's status. A stored lead alone does not confirm email arrival. Finish the app's Test > Run checks > Activate flow.
| If you see | It means |
|---|---|
401 missing service key |
The X-Api-Key header did not reach Buckhy. |
401 invalid or revoked developer key |
Wrong, expired or revoked key. Deploy the stored value or create an intentional replacement; Buckhy cannot redisplay it. |
403 this key cannot capture form submissions |
The key belongs to another product. Use the key from the Form Capture site. |
403 CORS_ORIGIN_DENIED |
The call came from a browser. Move it to your server. |
400 fields are required |
fields was missing, empty, or not an object. |
