SDK

JavaScript SDK

Official client libraries for submitting to ShipMyForm over fetch, with typed results and idiomatic bindings for every major framework. Your forms keep working without JavaScript and upgrade to fetch when it is available.

You never need the SDK: A plain <form action> works everywhere. Reach for the SDK when you want an inline success state, typed error handling, or a hook that fits your framework, not because submissions require it.

Packages

Everything is published under the @shipmyform scope on npm. Install only the one for your stack. Each framework package pulls in @shipmyform/core for you.

PackageUse withAPI
@shipmyform/coreAny framework / vanilla JScreateClient, enhance
@shipmyform/reactReact, Next.jsuseSubmit hook
@shipmyform/vueVue 3useSubmit composable
@shipmyform/svelteSvelte, SvelteKitcreateSubmit, use:shipmyform
@shipmyform/solidSolidcreateSubmit primitive
@shipmyform/astroAstro<ShipMyForm> component

Core client

The framework-agnostic client. submit accepts a plain object, a FormData, or an HTMLFormElement. Passing a File or Blob switches the request to multipart/form-data automatically.

submit.ts
import { createClient } from "@shipmyform/core";

const form = createClient({ formId: "frm_8Kx2mQ9pL4vN" });

const result = await form.submit({
  email: "[email protected]",
  message: "Hello",
});

if (result.ok) {
  // stored and routed
} else {
  console.error(result.error, result.message);
}

React

npm i @shipmyform/react

Contact.tsx
import { useSubmit } from "@shipmyform/react";

export function Contact() {
  const { submit, submitting, succeeded, error } = useSubmit({
    formId: "frm_8Kx2mQ9pL4vN",
  });

  if (succeeded) return <p>Thanks, we&apos;ll be in touch!</p>;

  return (
    <form onSubmit={(e) => { e.preventDefault(); submit(e.currentTarget); }}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button disabled={submitting}>Send</button>
      {error && <p role="alert">{error.message}</p>}
    </form>
  );
}

Vue

npm i @shipmyform/vue

Contact.vue
<script setup lang="ts">
import { useSubmit } from "@shipmyform/vue";

const { submit, submitting, succeeded, error } = useSubmit({
  formId: "frm_8Kx2mQ9pL4vN",
});
</script>

<template>
  <p v-if="succeeded">Thanks!</p>
  <form v-else @submit.prevent="submit($event.target as HTMLFormElement)">
    <input name="email" type="email" required />
    <textarea name="message" required />
    <button :disabled="submitting">Send</button>
    <p v-if="error" role="alert">{{ error.message }}</p>
  </form>
</template>

Svelte

npm i @shipmyform/svelte: a store for full control, or the use:shipmyform action for one-line progressive enhancement.

Contact.svelte
<script lang="ts">
  import { createSubmit } from "@shipmyform/svelte";
  const { submit, submitting, succeeded, error } = createSubmit({
    formId: "frm_8Kx2mQ9pL4vN",
  });
</script>

{#if $succeeded}
  <p>Thanks!</p>
{:else}
  <form on:submit|preventDefault={(e) => submit(e.currentTarget)}>
    <input name="email" type="email" required />
    <button disabled={$submitting}>Send</button>
    {#if $error}<p role="alert">{$error.message}</p>{/if}
  </form>
{/if}

Solid

npm i @shipmyform/solid

Contact.tsx
import { Show } from "solid-js";
import { createSubmit } from "@shipmyform/solid";

export function Contact() {
  const { submit, submitting, succeeded, error } = createSubmit({
    formId: "frm_8Kx2mQ9pL4vN",
  });

  return (
    <Show when={!succeeded()} fallback={<p>Thanks!</p>}>
      <form onSubmit={(e) => { e.preventDefault(); submit(e.currentTarget); }}>
        <input name="email" type="email" required />
        <button disabled={submitting()}>Send</button>
        <Show when={error()}>{(e) => <p role="alert">{e().message}</p>}</Show>
      </form>
    </Show>
  );
}

Astro

npm i @shipmyform/astro: renders a native form that posts without JavaScript and upgrades to fetch on the client. Props: formId (required), endpoint, class.

contact.astro
---
import ShipMyForm from "@shipmyform/astro/ShipMyForm.astro";
---

<ShipMyForm formId="frm_8Kx2mQ9pL4vN">
  <input name="email" type="email" required />
  <textarea name="message" required />
  <button>Send</button>
</ShipMyForm>

<script>
  document.addEventListener("shipmyform:success", () => alert("Thanks!"));
  document.addEventListener("shipmyform:error", (e) => console.error(e.detail));
</script>

Progressive enhancement

With vanilla core, enhance keeps a native <form> working without JavaScript and upgrades it to fetch when available.

enhance.ts
import { createClient, enhance } from "@shipmyform/core";

const el = document.querySelector("form");
const client = createClient({ formId: "frm_8Kx2mQ9pL4vN" });

enhance(el, client, {
  onSuccess: () => el.replaceWith("Thanks!"),
  onError: (e) => alert(e.message),
});

Options

Client-level options go to createClient; per-request options go to submit.

options.ts
const form = createClient({
  formId: "frm_8Kx2mQ9pL4vN",
  endpoint: "https://forms.acme.dev", // custom domain or self-host
  timeout: 10_000,                    // ms before the request aborts
});

await form.submit(data, {
  turnstileToken,  // Cloudflare Turnstile token, when the form requires one
  subject,         // override the email-notification subject
  redirect,        // redirect target used for the no-JS fallback
  signal,          // an AbortSignal to cancel the request
});

Result and errors

Every submit resolves to a discriminated union. It never throws for an expected failure, so you branch on result.ok.

result.ts
type SubmitResult =
  | { ok: true }
  | { ok: false; error: SubmitErrorCode; message: string; status: number };
errorMeaning
rate_limitedToo many submissions from this client. Try again shortly.
originThe request came from a domain that is not on the form's allowlist.
turnstileCloudflare Turnstile was required and the token was missing or invalid.
too_largeThe submission (or an uploaded file) exceeded the size limit.
emptyNo fields were submitted.
not_foundNo form matches that formId.
unavailableThe service was temporarily unavailable (5xx).
networkThe request never reached the server.
timeoutThe request exceeded the client timeout.
unknownAn unexpected error. See message for details.
Abuse protection: ShipMyForm's endpoint is public and cross-origin, so there is no CSRF token to fetch. The SDK keeps the honeypot (_gotcha) empty and forwards a Cloudflare Turnstile token when you pass one; the backend screens every request with an origin allowlist and rate limiting. See how spam protection works.

Source and changelog live on github.com/ShipMyForm/shipmyform-js.