Skip to content

Endpoint Management

An endpoint is a form destination. Submissions are only accepted for endpoints that exist. Every route on this page requires a management API key.

export HYMICAL_KEY=hym_live_REPLACE_WITH_YOUR_KEY

Creating 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"}'

Returns 201 Created:

{
  "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. Losing it means creating a new endpoint.

Reusing an ID returns 409 endpoint_already_exists.

Endpoint IDs are yours to choose

They are supplied by you, not generated, because the ID appears in the action URL of your HTML form and a memorable one is worth more than an opaque one.

An ID is 3 to 64 characters of lowercase ASCII letters, digits, - and _, and must start and end with a letter or digit.

The endpoint ID cannot be changed

It is the primary key, and it appears in the action URL of every HTML form pointing at the endpoint, so changing it would break deployed forms. An id in a PATCH body is ignored.

The key that created an endpoint is not recorded on it. A management key administers the service rather than owning a slice of it, and there is no tenancy model for an owner column to belong to.

Reading endpoints

curl "http://127.0.0.1:8000/endpoints?limit=2" \
  -H "Authorization: Bearer $HYMICAL_KEY"
{
  "items": [
    {
      "id": "contact-form",
      "name": "Contact form",
      "is_active": true,
      "created_at": "2026-08-24T14:34:27.432598Z",
      "webhook_url": "https://example.com/hooks/forms"
    }
  ],
  "next_cursor": null
}

webhook_url is returned because it is configuration rather than a credential: knowing where an endpoint delivers proves nothing and forges nothing. The signing secret is not returned, here or anywhere else.

GET /endpoints/{endpoint_id} gives the same representation for one endpoint, so a caller that already knows an ID does not have to page to find it. An unknown ID returns 404 endpoint_not_found.

Listing is paginated. See Pagination.

Changing an endpoint

PATCH rather than PUT, because only a few fields are yours to change. Send only what should change; anything omitted is left exactly as it was.

curl -X PATCH http://127.0.0.1:8000/endpoints/contact-form \
  -H "Authorization: Bearer $HYMICAL_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name": "Support form", "is_active": false}'
Field Meaning
name New label, 1 to 200 characters
is_active Whether the endpoint accepts submissions
webhook_url New destination, or null to remove the webhook entirely

Disabling and re-enabling

curl -X PATCH http://127.0.0.1:8000/endpoints/contact-form \
  -H "Authorization: Bearer $HYMICAL_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"is_active": false}'

Disabling takes effect on the very next submission, which is refused with 409 endpoint_inactive and stores nothing. Nothing caches the endpoint, so there is no window in which a disabled endpoint still accepts a form. Sending {"is_active": true} restores acceptance just as immediately.

Deliveries that were already queued are not affected. They are work this service already promised to do, and disabling an endpoint stops it taking on more rather than abandoning what it owes.

Disabled endpoints still spend rate limit budget

An attempt against a disabled endpoint is charged to both limiters before the active check runs, so a disabled endpoint cannot be used as a free target. See Rate limiting.

Changing the webhook

The destination and its signing secret change together, because a secret belongs to a receiver rather than to an endpoint:

Change What happens to the secret
webhook_url omitted Unchanged
webhook_url set to the same URL Unchanged
webhook_url set to a different URL A new one is generated and returned
webhook_url set on an endpoint with no webhook A new one is generated and returned
webhook_url set to null Removed along with the destination
{
  "id": "contact-form",
  "name": "Contact form",
  "is_active": true,
  "created_at": "2026-08-24T14:34:27.432598Z",
  "webhook_url": "https://example.com/hooks/forms-v2",
  "webhook_secret": "whsec_6f1c...  (64 hex characters)"
}

webhook_secret is null when the request did not generate one, and the field does not exist at all on a read.

Carrying the old secret over to a new destination would hand a receiver that never had it the ability to verify signatures, and leave the previous receiver holding a live secret. So the two always move together.

A signing secret cannot be rotated in place

Rotation happens only as a side effect of changing the destination. Re-keying a receiver that stays at the same URL means pointing the endpoint elsewhere and back, or standing up a second URL. A dedicated rotate action is not implemented.

Deliveries already queued keep the destination and secret they snapshotted when their submission was accepted. Changing configuration here never redirects work that is already owed, and never leaves a queued payload signed with a secret its receiver never had.

Deleting an endpoint

Not implemented. Disabling one is enough for now: deletion immediately raises what happens to its stored submissions, its delivery history and the foreign keys between them, and none of that is decided yet.