Fifteen tools cover forms, their submissions, and webhook delivery — the whole Web3Forms-style pipeline: define a form, accept submissions, notify or reply, and export.
All endpoints follow the pattern POST https://mcp.vlozi.app/tools/forms.<name> with Authorization: Bearer ls_xxx. All responses use this envelope:
{ "data": <payload>, "error": null } // success
{ "data": null, "error": "..." } // failure (with HTTP 4xx/5xx)NOTE
Every accepted (non-spam) submission costs 0.25 credit, metered as you go and settled to the ledger in whole-credit increments — submitting is blocked with 402 once your balance can't cover it. File-field uploads are proxied through the media service; a file over 25MB or 5 files rejects the whole submission, while a transient media-service failure fails soft (the filename is kept, the file isn't).
Forms
forms.list_forms
List forms in the workspace.
Scope: forms:read
Input: none
Response: { forms: { id, name, slug, status, captchaRequired, submissionCount, createdAt, updatedAt }[] }
forms.get_analytics
Submission analytics — a workspace overview, or one form's detail.
Scope: forms:read
Input: { formId?: string, days?: 7 | 30 | 90 } (default 30; anything else is coerced to 30)
Response (no formId): { counts: { total, new, seen, handled, spam }, deltas: {...}, byForm: { id, name, count }[], series: { date, count }[], views: number, conversion: number | null, usage: { used, limit: -1 }, credits: { balance, spentThisMonth, perSubmission: 0.25, pending } | null }
Response (with formId): { form: { id, name }, counts: {...}, totals: { views, submissions, conversion }, previous: { views, submissions }, series: { date, views, submissions }[], days }
NOTE
usage.limit always reports -1 (unlimited) in this payload even on plans with a real monthly submission cap — the cap is enforced elsewhere, this overview just doesn't reflect it yet.
forms.get_form
Get a form's full definition, including its webhook signing secret.
Scope: forms:read
Input: { id: string } (required)
Response: { form: { id, name, slug, schema, settings, notify, webhookUrl, webhookSecret, allowedOrigins, honeypotField, captchaRequired, status } }
forms.create_form
Create a new form.
Scope: forms:write
Input:
| Field | Type | Notes |
|---|---|---|
name |
string (required) |
1–200 chars |
schema |
{ fields: FormField[] } |
Omit/empty to accept any posted fields |
settings |
{ successMessage?, redirectUrl?, theme?, turnstileSiteKey? } |
|
notify |
{ email?: { enabled, to? }, newsletterListId? } |
|
webhookUrl |
string |
|
allowedOrigins |
string[] |
Max 50 |
honeypotField |
string |
Default "botcheck" |
captchaRequired |
boolean |
Default false |
Response: { form: object } — a webhookSecret is generated automatically if webhookUrl is set
WARNING
captchaRequired: true is rejected unless settings.turnstileSiteKey is also provided — otherwise every submission would fail captcha silently. webhookUrl can't point at api.vlozi.app (self-loop guard).
forms.update_form
Update a form. Pass only the fields to change.
Scope: forms:write
Input: { id: string } plus any subset of create_form's fields, plus status?: "active" | "paused" | "archived"
Response: { form: object }
NOTE
Setting a webhookUrl for the first time mints a fresh webhookSecret.
forms.delete_form
Archive a form.
Scope: forms:write
Input: { id: string } (required)
Response: { id: string, status: "deleted" }
NOTE
Soft delete — existing submissions are kept, just the form itself stops accepting new ones.
forms.duplicate_form
Clone a form's configuration into a new one named "<original> (copy)".
Scope: forms:write
Input: { id: string } (required)
Response: { form: object } — the new form, with its own id/slug/webhook secret
Submissions
forms.list_submissions
List submissions across all forms (the global inbox) or one form.
Scope: forms:read
Input: { status?: "new" | "seen" | "handled" | "spam", formId?: string, q?: string, page?: number, limit?: number } (limit default 25, max 100)
Response: { data: object[], meta: { page, limit, total, totalPages, counts: { all, new, seen, handled, spam } } }
NOTE
q searches submission data and internal notes. counts always reflects the other filters but ignores status, so tab counts show what you'd see clicking each tab.
forms.get_submission
Get one submission.
Scope: forms:read
Input: { id: string } (required)
Response: { submission: { id, formId, data, meta, status, spamReason, note, sourceIp, userAgent, submittedAt, handledAt } }
NOTE
File-field values appear as { __file: true, mediaId, name, size, type, url }.
forms.update_submission
Update a submission's status or note.
Scope: forms:write
Input: { id: string, status?: "new" | "seen" | "handled" | "spam", note?: string } (at least one of status/note required)
Response: { submission: { id, formId, status, note, handledAt } }
NOTE
Setting status: "handled" stamps handledAt; any other status clears it.
forms.delete_submission
Permanently delete a submission.
Scope: forms:write
Input: { id: string } (required)
Response: { id: string, status: "deleted" }
CAUTION
Hard delete — no restore, unlike blog posts.
forms.reply_submission
Email a reply to whoever submitted the form.
Scope: forms:write
Input: { id: string, subject?: string, message: string } (message required; subject defaults to "Re: your submission")
Response: { sent: true }
IMPORTANT
Requires the submission to have a plain-string email field — returns an error if it doesn't. Sends through the comms service (real email, not a webhook), and marks the submission handled on success. Returns 502 if the send fails, including if the recipient is on your suppression list.
forms.export_submissions
Export every submission for a form.
Scope: forms:submissions.export
Input: { formId: string } (required)
Response: { submissions: object[] } — all submissions, oldest first, as inline JSON
NOTE
This returns JSON directly in the response, not a file URL or CSV — for a CSV export, use the dashboard's own export button instead.
Webhooks
forms.list_webhook_deliveries
List recent webhook delivery attempts for a form.
Scope: forms:read
Input: { formId: string } (required)
Response: { deliveries: { id, submissionId, url, attempt, statusCode, responseBody, status: "pending" | "success" | "failed" | "retrying", deliveredAt, createdAt }[] } — most recent 20
forms.retry_webhook_delivery
Retry a failed webhook delivery.
Scope: forms:write
Input: { id: string } (required — the delivery id, not the submission id)
Response: { retried: true }
NOTE
Redelivers to the submission using the form's current webhook config (up to 3 attempts with backoff) — if the webhook URL has since been removed, this fails with "This form has no webhook URL". Each retry writes a new delivery record rather than overwriting the original failed one.