Skip to content

Quick Start

From a clone to an accepted submission. Assumes you have followed Installation and have a database URL exported.

1. Migrate the database

alembic upgrade head

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.

python -m hymical_forms.cli create-key --name local-admin
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.

export HYMICAL_KEY=hym_live_REPLACE_WITH_YOUR_KEY

3. Start the API

uvicorn hymical_forms.main:app --reload

Interactive API documentation is served at http://127.0.0.1:8000/docs.

4. Start the worker

In a second terminal:

python -m hymical_forms.worker

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:

curl -i -X POST http://127.0.0.1:8000/f/contact-form \
  -d email=dev@example.com -d message=hello
{
  "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"}'
curl -X POST http://127.0.0.1:8000/f/broken-demo -d hello=world

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