Skip to main content
It keeps the fields in Svelte 5 runes, posts them as JSON and shows formcarry’s answer, and the same request carries files, a spam blocker token and validation errors, in the browser or from a SvelteKit form action.

Prerequisites

Before you start, you need:
  • A formcarry account. Sign up is free.
  • A form in the dashboard. Its endpoint is on the form’s Setup page. The examples use https://formcarry.com/s/AbC123xyz; put yours in its place.

The component

To send a form from Svelte, keep the fields in state, post them as JSON, and send Accept: application/json so the answer comes back as JSON:
The keys of the JSON body are the field names formcarry stores. The email key is the visitor’s address, so it becomes the reply to address of your notification email and the recipient of the auto response. A stored submission answers with code: 200. A refused one answers with the reason in message, see What every form needs. Send Accept: application/json rather than Content-Type alone, otherwise the answer is the thank you page’s HTML and res.json() throws.

Files

To send files, build a FormData from the form element and leave the content type to the browser:
FormData reads the values from the elements, so every input needs a name, the file input too. Send Accept and nothing else rather than adding Content-Type yourself, otherwise the multipart boundary is missing and the upload fails. For several files, bind the input’s files, build the FormData yourself and append each file under its own name:
For small files you can stay with JSON and send the file as a data URL. Read it with FileReader, and give each file its own key, never an array:
Use data URLs for small files. Using them with larger files bloats the request by about a third and reaches the 50 MB limit sooner.
Files are stored on paid plans only. Free plans store the rest of the submission without them.

SvelteKit

To keep the form working before the JavaScript loads, keep action and method="POST" on the form and read the endpoint from e.currentTarget.action in the handler:
Until the JavaScript loads, the browser posts the form itself and the visitor lands on formcarry’s thank you page or your redirect. Write this handler rather than use:enhance, otherwise SvelteKit reads formcarry’s answer as one of its own form action results and form in your page never updates. To post from the server instead, write a form action in +page.server.js:
In +page.svelte, post with method="POST" and no action, and read the action’s answer from form:
A refusal arrives as form.message. use:enhance belongs here, on a form that posts to your own action, and keeps the page from reloading on each submission. Every submission now leaves your server, so all your visitors share its IP address and the 1 submission per 15 seconds per form per IP address limit applies to all of them together. The spam blocker token still has to come from the browser, so its widget stays in the form.
We recommend posting from the browser for public forms. The limit then applies to each visitor on their own, giving them their own 15-second window.

Spam blocker

To add a challenge, render the vendor’s widget, keep its token in state, send it under the name formcarry reads, and paste the secret key into the form’s settings under Form Security: With Turnstile, load its script before the component mounts, then render the widget into an element you bind:
reCAPTCHA v2 and hCaptcha render the same way, through grecaptcha.render and hcaptcha.render. With v3 there is no widget. Ask for the token when the visitor submits:
Refuse to send while the token is empty rather than posting anyway, otherwise formcarry answers 403 (reCAPTCHA) or 400 (the others). The guard goes at the top of the handler:
Add localhost to the challenge’s allowed domains while you test.

Validation errors

A 422 carries errors, one entry per failing field, each with a message. To show each one next to its field, add a branch to the handler:

What’s next

Stuck? Write to help@formcarry.com. Include the form id.