Is the project key a secret?
No. It belongs in the HTML of a public page. It allows submissions to that one project and nothing else — reading the inbox requires your signed-in account.
One POST URL per project, two body formats, ten possible answers, and five limits. This is the whole contract, written from the code that enforces it.
POST https://www.submitharbor.com/api/submit/sh_your_project_key
└────────────────────────────┘└──────────────────┘
submission route your project keysh_ for a project you created, demo_ for the shared demo. A key that does not match the expected shape is rejected with 404 without any lookup.POST to submit and OPTIONS for the CORS preflight. Anything else is not handled.The endpoint reads the Content-Type and parses accordingly. There is no separate URL per format.
| Content-Type | Parsed as | Notes |
|---|---|---|
| application/json | A flat object | String and number values are read and trimmed. Nested objects and arrays are ignored, not stored. |
| application/x-www-form-urlencoded | Form fields | What a native HTML form sends by default. |
| multipart/form-data | Form fields | Text fields are read. File parts are not stored — there are no attachments. |
A JSON submission from a browser triggers a preflight. It is answered before any project lookup, so it is fast and never leaks whether a key exists.
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
Vary: OriginChecks run in this order: key shape, project lookup, origin, rate limit, size, parse, honeypot, validation, storage.
| Response | Meaning | When you get it |
|---|---|---|
| 201 Created | Accepted | The submission passed validation. The JSON body reports whether it was stored and includes an X-RateLimit-Remaining header. |
| 200 OK | Filtered | A honeypot field was populated. The response reports filtered: true and stored: false so a bot cannot tell it was caught. |
| 303 See Other | Redirected | A stored submission included a _redirect target on the submitting origin or the project allow-list, so the browser is sent to your own thank-you page. |
| 400 Bad Request | Unreadable body | The body was neither a JSON object nor ordinary form data. |
| 403 Forbidden | Origin not allowed | The project has an origin allow-list and the request came from a site that is not on it. |
| 404 Not Found | Unknown endpoint | The key is malformed, or no project exists behind a well-formed key. |
| 413 Payload Too Large | Body too big | The request declared more than 64 KB of content. |
| 422 Unprocessable Content | Validation failed | A field limit was exceeded, a field name was unusable, or email and message did not pass their checks. The error string is safe to show a visitor. |
| 429 Too Many Requests | Rate limited | More than 20 submissions per minute from one address to one endpoint. A Retry-After header is included. |
| 503 Service Unavailable | Storage unavailable | The submission was valid but the configuration lookup or the write did not complete. Nothing was silently dropped. |
| Limit | Value | What happens at the edge |
|---|---|---|
| Request body | 64 KB | Larger declared payloads are rejected with 413. |
| Fields per submission | 20 | Counted after control fields are removed; 422 beyond it. |
| Characters per field | 5,000 | Values are trimmed first, then checked; 422 beyond it. |
| Field name | 1–50 characters | Letters, numbers, underscores, and hyphens only. |
| Submissions per minute | 20 per address, per endpoint | A fixed 60-second window, then 429. |
A successful submission returns an X-RateLimit-Remaining header, so a client can back off before it is refused rather than after.
Two ways to exercise the endpoint, both hitting the same code that serves production traffic.
curl -i -X POST https://www.submitharbor.com/api/submit/demo_contact_7x2p \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","message":"Testing the demo endpoint."}'Or open the studio and use the test form on the setup screen. It posts to the endpoint over the network like any other client and shows you the captured payload that came back, including the origin and the timestamp.
The shared demo key validates a submission and echoes it back, then discards it. It never writes to the database, because a stored demo submission would leave a stranger's details in an inbox nobody owns. Create your own endpoint to keep what arrives.
Worth reading before you build on it. These limits apply today.
No. It belongs in the HTML of a public page. It allows submissions to that one project and nothing else — reading the inbox requires your signed-in account.
201, with a JSON body reporting whether it was stored, which mode handled it, and how much of the rate-limit window is left.
No. Multipart bodies are accepted and their text fields are read, but file parts are not stored. There are no attachments.