Skip to main content
Technology & EngineeringForm Survey Services235 lines

Jotform

Integrate JotForm's REST API to create forms, retrieve submissions, process

Quick Summary30 lines
You are an expert at integrating JotForm via its REST API. You build and manage forms programmatically, retrieve and process submissions, handle payment integrations, and react to events through webhooks with type-safe TypeScript patterns.

## Key Points

- **Ignoring `payment.status`** on payment forms. Submission existence does not mean payment succeeded — always check the payment object.
- **Using question array index** instead of question ID or type to locate answers. Indices change when questions are reordered.
- **Fetching all submissions without pagination**. Forms with thousands of entries will time out or hit rate limits.
- **Storing JotForm data without the submission ID**. You need it for updates, deletions, and deduplication.
- Building form-driven workflows where submissions trigger backend processes (CRM updates, order fulfillment).
- Integrating payment collection forms with order management systems.
- Programmatically creating many similar forms from templates.
- Migrating or syncing JotForm submission data to external databases.
- Adding webhook-based automation to existing JotForm setups.

## Quick Example

```bash
JOTFORM_API_KEY=your-api-key
# Use EU endpoint if your account is in EU:
# JOTFORM_BASE_URL=https://eu-api.jotform.com
```

```typescript
await jotformFetch(`/form/${formId}/webhooks`, {
  method: "POST",
  body: new URLSearchParams({ webhookURL: "https://myapp.com/webhooks/jotform" }),
});
```
skilldb get form-survey-services-skills/JotformFull skill: 235 lines
Paste into your CLAUDE.md or agent config

JotForm API Skill

You are an expert at integrating JotForm via its REST API. You build and manage forms programmatically, retrieve and process submissions, handle payment integrations, and react to events through webhooks with type-safe TypeScript patterns.

Core Philosophy

Resource-Oriented API Design

JotForm's API is RESTful with clear resource hierarchies: users own forms, forms contain questions and submissions. Navigate this hierarchy consistently — always scope requests by form ID, and use query parameters for filtering rather than fetching everything client-side.

Submission as Source of Truth

Treat JotForm submissions as your canonical data source. Each submission has a unique ID, a status, and a created date. Use the submission endpoint for backfills and webhooks for real-time processing. Always store the JotForm submission ID to enable updates and lookups.

Payment-Aware Form Handling

JotForm natively integrates with payment gateways (Stripe, PayPal, Square). When processing payment forms, always check the payment object in submissions for transaction status before fulfilling orders. Never assume payment succeeded based on submission existence alone.

Setup

Configure API authentication:

const JOTFORM_API_KEY = process.env.JOTFORM_API_KEY;
const BASE_URL = "https://api.jotform.com";

async function jotformFetch<T>(path: string, init?: RequestInit): Promise<T> {
  const url = new URL(`${BASE_URL}${path}`);
  url.searchParams.set("apiKey", JOTFORM_API_KEY!);
  const res = await fetch(url.toString(), {
    ...init,
    headers: { "Content-Type": "application/json", ...init?.headers },
  });
  if (!res.ok) throw new Error(`JotForm ${res.status}: ${await res.text()}`);
  const json = await res.json();
  return json.content as T;
}

Environment variables:

JOTFORM_API_KEY=your-api-key
# Use EU endpoint if your account is in EU:
# JOTFORM_BASE_URL=https://eu-api.jotform.com

Key Patterns

Retrieve and Process Submissions

Do this — paginate through submissions with filtering:

interface JotFormSubmission {
  id: string;
  form_id: string;
  status: "ACTIVE" | "DELETED" | "OVERQUOTA";
  created_at: string;
  answers: Record<string, {
    name: string;
    text: string;
    answer: string | Record<string, string>;
    type: string;
    prettyFormat?: string;
  }>;
  payment?: {
    total: string;
    currency: string;
    status: "PAID" | "PENDING" | "FAILED";
    transactionId: string;
  };
}

async function getSubmissions(
  formId: string,
  opts: { offset?: number; limit?: number; status?: string } = {}
): Promise<JotFormSubmission[]> {
  const params = new URLSearchParams({
    offset: String(opts.offset ?? 0),
    limit: String(opts.limit ?? 100),
    ...(opts.status ? { "filter[status]": opts.status } : {}),
  });
  return jotformFetch<JotFormSubmission[]>(`/form/${formId}/submissions?${params}`);
}

// Usage
const activeSubmissions = await getSubmissions("240000000000", { status: "ACTIVE", limit: 50 });
for (const sub of activeSubmissions) {
  const email = Object.values(sub.answers).find((a) => a.type === "control_email")?.answer;
  console.log(`Submission ${sub.id}: ${email}`);
}

Not this — fetching all submissions without pagination or filtering, causing timeouts on high-volume forms.

Handle Payment Submissions

Do this — verify payment status before fulfillment:

async function processPaymentSubmission(submissionId: string): Promise<void> {
  const submission = await jotformFetch<JotFormSubmission>(`/submission/${submissionId}`);

  if (!submission.payment) {
    throw new Error("Not a payment submission");
  }

  switch (submission.payment.status) {
    case "PAID":
      await fulfillOrder({
        transactionId: submission.payment.transactionId,
        amount: parseFloat(submission.payment.total),
        currency: submission.payment.currency,
        answers: submission.answers,
      });
      break;
    case "PENDING":
      await markOrderPending(submissionId);
      break;
    case "FAILED":
      await notifyPaymentFailure(submissionId);
      break;
  }
}

Not this — treating every submission as a successful purchase without checking payment.status.

Create Forms with Questions Programmatically

Do this — build forms via the API with typed question properties:

interface JotFormQuestion {
  type: "control_email" | "control_textbox" | "control_dropdown" | "control_fullname" | "control_payment";
  text: string;
  name: string;
  required: "Yes" | "No";
  order: string;
  options?: string;  // pipe-separated for dropdowns
}

const formId = await jotformFetch<string>("/user/forms", {
  method: "POST",
  body: new URLSearchParams({
    "properties[title]": "Order Form",
    "questions[1][type]": "control_fullname",
    "questions[1][text]": "Full Name",
    "questions[1][name]": "fullName",
    "questions[1][required]": "Yes",
    "questions[1][order]": "1",
    "questions[2][type]": "control_email",
    "questions[2][text]": "Email",
    "questions[2][name]": "email",
    "questions[2][required]": "Yes",
    "questions[2][order]": "2",
    "questions[3][type]": "control_dropdown",
    "questions[3][text]": "Plan",
    "questions[3][name]": "plan",
    "questions[3][options]": "Starter|Pro|Enterprise",
    "questions[3][order]": "3",
  }),
});

Not this — manually creating forms in the UI when you need many similar forms; use the API for templated creation.

Common Patterns

Register a Webhook

await jotformFetch(`/form/${formId}/webhooks`, {
  method: "POST",
  body: new URLSearchParams({ webhookURL: "https://myapp.com/webhooks/jotform" }),
});

Get Form Questions

interface JotFormQuestionMap {
  [questionId: string]: JotFormQuestion & { qid: string };
}

const questions = await jotformFetch<JotFormQuestionMap>(`/form/${formId}/questions`);
const emailQuestion = Object.values(questions).find((q) => q.type === "control_email");

Update Submission Status

await jotformFetch(`/submission/${submissionId}`, {
  method: "POST",
  body: new URLSearchParams({ "submission[status]": "ACTIVE" }),
});

List User Forms with Search

interface JotFormMeta {
  id: string;
  title: string;
  status: "ENABLED" | "DISABLED" | "DELETED";
  count: string;  // submission count as string
  created_at: string;
}

const forms = await jotformFetch<JotFormMeta[]>(
  `/user/forms?filter={"status":"ENABLED"}&orderby=created_at`
);

Anti-Patterns

  • Ignoring payment.status on payment forms. Submission existence does not mean payment succeeded — always check the payment object.
  • Using question array index instead of question ID or type to locate answers. Indices change when questions are reordered.
  • Fetching all submissions without pagination. Forms with thousands of entries will time out or hit rate limits.
  • Storing JotForm data without the submission ID. You need it for updates, deletions, and deduplication.

When to Use

  • Building form-driven workflows where submissions trigger backend processes (CRM updates, order fulfillment).
  • Integrating payment collection forms with order management systems.
  • Programmatically creating many similar forms from templates.
  • Migrating or syncing JotForm submission data to external databases.
  • Adding webhook-based automation to existing JotForm setups.

Install this skill directly: skilldb add form-survey-services-skills

Get CLI access →