# Client-side custom forms

Register dynamic form schemas with the QwryAI JavaScript API.

## registerFormSchema

After loading the embed, register every form in one call. Each call replaces the entire form registry. The async function receives the action's extracted args and the same user object as registerTools: user_id, user_hash, anon_user_id and user_metadata.

```javascript
window.qwryai("registerFormSchema", {
  learn_more_form: async (args, user) => ({
    fields: [
      { name: "email", label: "Email address", type: "email",
        defaultValue: user.user_metadata.email || "",
        validation: { required: { value: true, message: "Email is required" },
          defaultErrorMessage: "Enter a valid email address" } },
      { name: "message", label: "Message", type: "textarea",
        defaultValue: args.topic || "" }
    ],
    showLabels: true,
    submitButtonText: "Send Message"
  })
});
```

## Schema

| Property | Default / behavior |
| --- | --- |
| fields | Array of name, label, type, placeholder, defaultValue, disabled, validation and options. |
| submitButtonText | Submit |
| showLabels | false |
| successMessage | Form submitted successfully |
| errorMessage | Error submitting form |
| placeholder | The field label |
| disabled | false |

## Field types

Supported types: text, textarea, email, phone, number, select, multiselect, groupselect, groupmultiselect and image. Select options use [{value, label}]. Grouped options use {Group: [{value, label}]}. Phone values use +[country code][number]. Image fields accept JPEG/JPG, PNG, GIF and WebP up to 2 MB, with drag and drop and a preview. Submitted images are uploaded through the conversation attachment service and represented by their attachment IDs.

## Validation

Use validation.required, pattern, minLength, maxLength, min or max, each with {value, message}. Set validation.defaultErrorMessage for format errors or rules without their own message. Email and phone formats are checked automatically. Required labels end in an asterisk.

## Submission and webhooks

The widget requests the schema from the host, renders it below the AI text and validates before submission. The server completes the action with the submitted data, then resumes the AI. The form is replaced by successMessage or errorMessage. An unregistered name logs Form [name] not found and fails the action without a customer-facing banner.

The event is <action name>_custom_form.submit. The POST body is {conversationId, data, error}; error is null unless an attachment failed to upload. The x-qwryai-signature header is the hexadecimal HMAC-SHA1 of the exact JSON body using that webhook's secret. Delivery is queued with retries and an Idempotency-Key identifying the submission.
