Reference
Form Capture API
The complete contract for POST /v1/capture: authentication, request body, field rules, responses, idempotency, rate limits and data retention.
Endpoint
| Method | URL | Purpose |
|---|---|---|
POST |
https://api.buckhy.com/v1/capture |
Store one form submission for the site the key belongs to |
Requests and responses are JSON. Every response carries a success boolean; failures add a human-readable message (see Errors).
Authentication
POST /v1/capture HTTP/1.1
Host: api.buckhy.com
Content-Type: application/json
X-Api-Key: bk_live_…
Idempotency-Key: 5d3f0c2e-6a1b-4c8e-9e2f-0b1a2c3d4e5f
X-Api-Keycarries the site key. The key is a secret: send it from your server only.X-Service-Keyis accepted as an alias.- Keys are verified by hash; the plaintext is never stored.
- The key must belong to a Form Capture site whose status is
active.
Call the endpoint server to server. Browser requests from your own domain are refused by CORS (403, code CORS_ORIGIN_DENIED).
Request body
{
"form": "partner",
"page": "/partner?utm_source=ig",
"fields": {
"name": "Priya Sharma",
"email": "priya@example.com",
"company": "Midnight Strings",
"message": "We'd like to perform at the September show."
},
"context": {
"ip": "203.0.113.24",
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) …",
"referrer": "https://www.instagram.com/",
"landingPage": "/events/acoustic-show",
"language": "en-IN",
"timezone": "Asia/Kolkata",
"screen": "390x844",
"utm": { "source": "ig", "medium": "social", "campaign": "sept" }
}
}
| Property | Type | Required | Rules |
|---|---|---|---|
fields |
object | yes | At least one field. See Field rules. |
form |
string | no | ≤ 40 characters. Defaults to the site's default form (contact unless changed). |
page |
string | no | ≤ 200 characters. The page the form was submitted from. |
context |
object | no | Visitor details; see Visitor context. |
Unknown top-level properties are ignored.
Field rules
fields is a flat object of strings. Buckhy sanitises it before storing:
| Rule | Detail |
|---|---|
| Maximum fields | 20 per submission — extra fields are dropped in object order. |
| Field names | Trimmed to 40 characters; only A–Z a–z 0–9 _ - are kept. A name that becomes empty is dropped. |
| Values | Converted to strings, trimmed, cut at 2,000 characters. null and undefined values are dropped. |
| Nested objects / arrays | Stringified. Send flat fields. |
| Honeypots | Not filtered here — drop your honeypot field on your server before posting. |
Field names are free-form; the console searches every value regardless of the name and picks the contact from common names (name, fullName, email, company, …).
Visitor context
context is optional. Each property is validated independently and dropped if malformed.
| Property | Accepted | Stored as |
|---|---|---|
ip |
IPv4 or IPv6 literal | Derived country / countryCode; the raw address is kept for 30 days, then removed. |
userAgent |
≤ 500 chars | Derived device label plus the raw string. |
referrer |
http(s):// URL ≤ 500 |
As sent. |
landingPage |
Path beginning with / ≤ 300 |
As sent. |
language |
BCP 47 tag | As sent. |
timezone |
IANA zone name | As sent. |
screen |
<w>x<h>, 2–5 digits each |
As sent. |
utm |
source medium campaign term content, each ≤ 120 |
As sent; empty keys dropped. |
Country lookup uses an offline database and fails open: if the address cannot be resolved the submission is still stored, with country: null.
Responses
| Status | When | Body |
|---|---|---|
201 Created |
Stored | { "success": true, "id": "…" } |
200 OK |
Replay of an Idempotency-Key with identical content |
{ "success": true, "id": "…", "duplicate": true } |
400 |
fields missing or empty |
{ "success": false, "message": "fields are required" } |
401 |
No key | { "success": false, "message": "missing service key" } |
401 |
Unknown or revoked key | { "success": false, "message": "invalid or revoked developer key" } |
403 |
Key is not a Form Capture key | { "success": false, "message": "this key cannot capture form submissions" } |
403 |
Site disabled | { "success": false, "message": "capture site is not active" } |
403 |
Browser request | { "message": "Not allowed by CORS", "code": "CORS_ORIGIN_DENIED" } |
409 |
Idempotency-Key reused with different content |
{ "success": false, "message": "this idempotency key was already used with different content" } |
429 |
Rate limit | { "success": false, "message": "Too many form submissions, please try again later" } |
503 |
Key verification unavailable | { "success": false, "message": "key verification unavailable" } — retry with backoff |
id is the submission's permanent identifier; it appears in the console's record drawer and CSV export.
Idempotency
Send an Idempotency-Key header to make retries safe.
- Format: 8–100 characters from
A–Z a–z 0–9 _ -. A UUID is ideal. Malformed keys are ignored, never rejected. - Scope: per site. The same key on two different sites is two different submissions.
- Match: the key plus a fingerprint of
form,pageandfields(field order does not matter). - Same key, same content →
200with the originalidandduplicate: true. One email, one record. - Same key, different content →
409. Generate a new key when the visitor edits and resubmits. - Concurrent identical retries are safe: a unique index elects one winner and the other request returns the winner's record.
Generate the key once per submission attempt in the browser, pass it through your server, and reuse it when you retry after a network error.
Rate limits
| Scope | Limit | Window |
|---|---|---|
| Per site key | 40 requests | 15 minutes |
Limits are reported with standard headers on every authenticated response:
RateLimit-Limit: 40
RateLimit-Remaining: 37
RateLimit-Reset: 812
Over the limit, requests return 429 with the body shown above. Back off until RateLimit-Reset (seconds) has elapsed. If you expect more than 40 submissions per 15 minutes on one site, queue on your side and drain at a steady rate.
Data retention
| Data | Retention |
|---|---|
| Submission fields, form, page, time, derived device and country | For the life of the site |
Raw visitor IP (context.ip) |
30 days, then removed by a scheduled job; the derived country remains |
| Site key | Stored as a SHA-256 hash only; revoked keys are kept as revoked for audit |
Exports from the console include everything retained for the filtered records.
