Quick Start¶
From a clone to an accepted submission. Assumes you have followed Installation and have a database URL exported.
1. Migrate the database¶
Both processes refuse to start against a schema they were not built for, so this comes first.
2. Create a management API key¶
Everything that administers the service needs a credential. Submitting a form does not.
Created management API key mk_634c4efc22fb40fab4b19b82202b23bb (local-admin).
hym_live_EXAMPLEONLYNOTAREALKEYREPLACETHISWITHYOURS
Save this key now. It is shown here and nowhere else: the server stores
only a digest of it and cannot show it again. If you lose it, create a
replacement and revoke this one by its key ID.
Save the key now
That line is the only time the credential is ever displayed. The server stores a SHA-256 digest and nothing else, so no command, route or query can show it to you again. See Authentication.
3. Start the API¶
Interactive API documentation is served at http://127.0.0.1:8000/docs.
4. Start the worker¶
In a second terminal:
The API accepts submissions, stores them, and records that a webhook is owed. It never makes an outbound request. The worker claims owed deliveries, sends them, and retries the ones that fail. Running the API alone is fine: submissions are still accepted and nothing is lost, they simply wait until a worker exists.
5. Register an endpoint¶
curl -X POST http://127.0.0.1:8000/endpoints \
-H "Authorization: Bearer $HYMICAL_KEY" \
-H 'Content-Type: application/json' \
-d '{"id": "contact-form", "name": "Contact form",
"webhook_url": "https://example.com/hooks/forms"}'
{
"id": "contact-form",
"name": "Contact form",
"is_active": true,
"created_at": "2026-08-24T14:34:27.432598Z",
"webhook_url": "https://example.com/hooks/forms",
"webhook_secret": "whsec_6f1c... (64 hex characters)"
}
Save webhook_secret now
It is generated by the server, returned only in this response, and there is no route that reads it back. It is what your receiver verifies signatures with. Losing it means creating a new endpoint.
webhook_url is optional. Leave it out and submissions are stored without any
delivery being owed.
6. Submit a form¶
Submitting needs no credential at all:
{
"submission_id": "sub_48984534f33749c49a88de2d59400dce",
"endpoint_id": "contact-form",
"received_at": "2026-08-24T14:34:27.651841Z",
"field_count": 2,
"idempotent_replay": false,
"delivery": { "queued": true }
}
202 Accepted, not 201 Created: the submission is stored and its delivery is
queued, but that delivery has not happened yet.
Or from a browser, against the locally running server:
<form action="http://127.0.0.1:8000/f/contact-form" method="POST">
<input type="email" name="email" required />
<textarea name="message"></textarea>
<button type="submit">Send</button>
</form>
7. Watch the delivery¶
curl "http://127.0.0.1:8000/deliveries?endpoint_id=contact-form" \
-H "Authorization: Bearer $HYMICAL_KEY"
The worker will have claimed it, attempted it, and either marked it delivered
or scheduled a retry. GET /deliveries/{delivery_id} shows the full attempt
history.
8. See a failed delivery, and replay it (optional)¶
Point an endpoint at a destination nothing is listening on, and give it a single
attempt, so it reaches failed right away instead of retrying for an hour first.
Stop the API and worker, export two more variables, and start them again:
export FORMS_ALLOW_PRIVATE_WEBHOOK_TARGETS=true # local demo only, never in production
export FORMS_WEBHOOK_MAX_ATTEMPTS=1
curl -X POST http://127.0.0.1:8000/endpoints \
-H "Authorization: Bearer $HYMICAL_KEY" \
-H 'Content-Type: application/json' \
-d '{"id": "broken-demo", "name": "Broken demo",
"webhook_url": "http://127.0.0.1:9/nothing-here"}'
Give the worker a moment to claim and attempt it, then look for it:
curl "http://127.0.0.1:8000/deliveries?endpoint_id=broken-demo&state=failed" \
-H "Authorization: Bearer $HYMICAL_KEY"
Take the id from the response and replay it:
curl -X POST http://127.0.0.1:8000/deliveries/whd_REPLACE_WITH_THE_ID/replay \
-H "Authorization: Bearer $HYMICAL_KEY"
It goes back to pending, and the worker attempts it again on its next poll. Against
this same broken destination it fails again, which is expected: the point of the
exercise is the state transition, failed to pending to failed, not a
successful send. Point webhook_url somewhere real to see a replay succeed. Full
detail: Delivery replay.
Where to go next¶
| You want to | Read |
|---|---|
| Understand what ingestion accepts | Form ingestion |
| Make client retries safe | Idempotency |
| Verify webhook signatures | Webhook delivery |
| Tune the public traffic limits | Rate limiting |
| Change or disable an endpoint | Endpoint management |
| Requeue a failed delivery | Delivery replay |