{
  "info": {
    "_postman_id": "ea5f0123-4567-89ab-cdef-easyformsapi1",
    "name": "EasyForms API",
    "description": "Form submission backend at https://easyfrm.dev — receive form submissions, deliver by email (via EasyEmails / EasyRender) and webhook. See https://easyfrm.dev/docs for the full reference.\n\n### Variables\n- `baseUrl` — defaults to https://easyfrm.dev. Change for self-hosted.\n- `apiKey` — Bearer key (starts with `ef_`). Created via /api/v1/apikeys.\n- `publicId` — short id used in the public submit URL (e.g. Crp6zkfAJV).\n- `formId` — internal form id (e.g. frm_...).\n- `submissionId` — internal submission id (e.g. sub_...).\n- `userEmail`, `userPassword` — for signup / login.\n\n### Flow\n1. Run **Account ▸ Sign up** then **Login** (sets session for /auth/me).\n2. Run **API keys ▸ Create (account)** — auto-saves `apiKey`.\n3. Run **Forms ▸ Create** — auto-saves `formId` + `publicId`.\n4. Run **Public submission ▸ POST /f/:publicId** — auto-saves `submissionId`.\n5. Everything else uses those variables.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      { "key": "token", "value": "{{apiKey}}", "type": "string" }
    ]
  },
  "variable": [
    { "key": "baseUrl",      "value": "https://easyfrm.dev", "type": "string" },
    { "key": "apiKey",       "value": "",                    "type": "string" },
    { "key": "publicId",     "value": "",                    "type": "string" },
    { "key": "formId",       "value": "",                    "type": "string" },
    { "key": "submissionId", "value": "",                    "type": "string" },
    { "key": "userEmail",    "value": "you@example.com",     "type": "string" },
    { "key": "userPassword", "value": "hunter2hunter2",      "type": "string" }
  ],
  "item": [
    {
      "name": "Public submission",
      "description": "The /f/:publicId endpoint takes the submissions from your HTML form or fetch() call.",
      "item": [
        {
          "name": "POST /f/:publicId (JSON)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const j = pm.response.json();",
                  "if (j && j.id) pm.collectionVariables.set('submissionId', j.id);",
                  "pm.test('200 ok', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ],
          "request": {
            "auth": { "type": "noauth" },
            "method": "POST",
            "header": [
              { "key": "content-type", "value": "application/json" },
              { "key": "accept",       "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"alice@example.com\",\n  \"message\": \"Hello from Postman!\"\n}"
            },
            "url": {
              "raw":  "{{baseUrl}}/f/{{publicId}}",
              "host": ["{{baseUrl}}"],
              "path": ["f", "{{publicId}}"]
            },
            "description": "Accepts any field. Declared `fieldSchema` fields are validated and type-coerced. The honeypot field, if configured, silently drops the submission when filled."
          },
          "response": [
            {
              "name": "200 OK",
              "originalRequest": {
                "method": "POST",
                "url": { "raw": "{{baseUrl}}/f/{{publicId}}" }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [{ "key": "content-type", "value": "application/json" }],
              "body": "{\n  \"ok\": true,\n  \"id\": \"sub_xyz123\"\n}"
            }
          ]
        },
        {
          "name": "POST /f/:publicId (urlencoded — classic HTML form)",
          "request": {
            "auth": { "type": "noauth" },
            "method": "POST",
            "header": [
              { "key": "content-type", "value": "application/x-www-form-urlencoded" }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                { "key": "email",   "value": "alice@example.com", "type": "text" },
                { "key": "message", "value": "Hello",             "type": "text" },
                { "key": "_url",    "value": "",                  "type": "text", "description": "Honeypot — leave empty" }
              ]
            },
            "url": {
              "raw":  "{{baseUrl}}/f/{{publicId}}",
              "host": ["{{baseUrl}}"],
              "path": ["f", "{{publicId}}"]
            },
            "description": "Server responds with a 302 to `config.redirectOkUrl` (or to the default thank-you JSON if none is configured)."
          }
        },
        {
          "name": "OPTIONS /f/:publicId (CORS preflight)",
          "request": {
            "auth": { "type": "noauth" },
            "method": "OPTIONS",
            "header": [
              { "key": "origin",                         "value": "https://example.com" },
              { "key": "access-control-request-method",  "value": "POST" },
              { "key": "access-control-request-headers", "value": "content-type" }
            ],
            "url": {
              "raw":  "{{baseUrl}}/f/{{publicId}}",
              "host": ["{{baseUrl}}"],
              "path": ["f", "{{publicId}}"]
            }
          }
        },
        {
          "name": "POST /f/:publicId/upload-url (Pro)",
          "request": {
            "auth": { "type": "noauth" },
            "method": "POST",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"filename\": \"screenshot.png\",\n  \"contentType\": \"image/png\",\n  \"size\": 184320\n}"
            },
            "url": {
              "raw":  "{{baseUrl}}/f/{{publicId}}/upload-url",
              "host": ["{{baseUrl}}"],
              "path": ["f", "{{publicId}}", "upload-url"]
            },
            "description": "Returns a presigned PUT URL to S3 valid for 10 minutes. PUT the file to `uploadUrl`, then submit the returned `key` as a regular field on POST /f/:publicId.\n\n402 uploads_not_on_plan if the owner's plan doesn't allow uploads.\n413 upload_too_large if size > plan.maxUploadBytes."
          }
        }
      ]
    },

    {
      "name": "Account",
      "item": [
        {
          "name": "POST /auth/signup",
          "request": {
            "auth": { "type": "noauth" },
            "method": "POST",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\":    \"{{userEmail}}\",\n  \"password\": \"{{userPassword}}\",\n  \"name\":     \"Postman tester\"\n}"
            },
            "url": {
              "raw":  "{{baseUrl}}/auth/signup",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "signup"]
            }
          },
          "response": [
            {
              "name": "200 OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "body": "{\n  \"ok\": true,\n  \"userId\": \"usr_...\",\n  \"emailVerificationSent\": true\n}"
            },
            {
              "name": "409 email_taken",
              "code": 409,
              "_postman_previewlanguage": "json",
              "body": "{ \"error\": \"email_taken\" }"
            }
          ]
        },
        {
          "name": "GET /auth/verify?token=…",
          "request": {
            "auth": { "type": "noauth" },
            "method": "GET",
            "url": {
              "raw":   "{{baseUrl}}/auth/verify?token=PASTE_TOKEN_HERE",
              "host":  ["{{baseUrl}}"],
              "path":  ["auth", "verify"],
              "query": [{ "key": "token", "value": "PASTE_TOKEN_HERE" }]
            }
          }
        },
        {
          "name": "POST /auth/login",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 ok', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ],
          "request": {
            "auth": { "type": "noauth" },
            "method": "POST",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\":    \"{{userEmail}}\",\n  \"password\": \"{{userPassword}}\"\n}"
            },
            "url": {
              "raw":  "{{baseUrl}}/auth/login",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "login"]
            },
            "description": "Sets the `ef_session` HttpOnly cookie. Use this for dashboard-style flows."
          }
        },
        {
          "name": "POST /auth/logout",
          "request": {
            "method": "POST",
            "url": {
              "raw":  "{{baseUrl}}/auth/logout",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "logout"]
            }
          }
        },
        {
          "name": "GET /auth/me",
          "request": {
            "method": "GET",
            "url": {
              "raw":  "{{baseUrl}}/auth/me",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "me"]
            },
            "description": "Returns `{user: null}` when not signed in."
          }
        },
        {
          "name": "POST /auth/forgot",
          "request": {
            "auth": { "type": "noauth" },
            "method": "POST",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{ \"email\": \"{{userEmail}}\" }"
            },
            "url": {
              "raw":  "{{baseUrl}}/auth/forgot",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "forgot"]
            },
            "description": "Always returns `{ok: true}` — we don't leak whether the email is registered."
          }
        },
        {
          "name": "POST /auth/reset",
          "request": {
            "auth": { "type": "noauth" },
            "method": "POST",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\":    \"PASTE_RESET_TOKEN\",\n  \"password\": \"new-password-min-8\"\n}"
            },
            "url": {
              "raw":  "{{baseUrl}}/auth/reset",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "reset"]
            }
          }
        }
      ]
    },

    {
      "name": "Forms",
      "item": [
        {
          "name": "GET /api/v1/forms",
          "request": {
            "method": "GET",
            "url": {
              "raw":   "{{baseUrl}}/api/v1/forms?tag=&q=",
              "host":  ["{{baseUrl}}"],
              "path":  ["api", "v1", "forms"],
              "query": [
                { "key": "tag", "value": "",       "disabled": true,  "description": "Filter by exact tag" },
                { "key": "q",   "value": "",       "disabled": true,  "description": "LIKE search over name and publicId" }
              ]
            }
          }
        },
        {
          "name": "POST /api/v1/forms",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const j = pm.response.json();",
                  "if (j && j.id)       pm.collectionVariables.set('formId',   j.id);",
                  "if (j && j.publicId) pm.collectionVariables.set('publicId', j.publicId);",
                  "pm.test('201 created', () => pm.response.to.have.status(201));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\":  \"Contact\",\n  \"tags\":  [\"leads\"],\n  \"config\": {\n    \"recipients\":      [\"me@example.com\"],\n    \"honeypotField\":   \"_url\",\n    \"redirectOkUrl\":   \"https://example.com/thanks\",\n    \"allowedOrigins\":  [\"https://example.com\"],\n    \"subjectTemplate\": \"New lead from {{data.email}}\",\n    \"replyToField\":    \"email\"\n  },\n  \"fieldSchema\": {\n    \"fields\": [\n      { \"name\": \"email\",   \"type\": \"email\",  \"required\": true },\n      { \"name\": \"message\", \"type\": \"string\", \"required\": true, \"min\": 3 }\n    ]\n  }\n}"
            },
            "url": {
              "raw":  "{{baseUrl}}/api/v1/forms",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "forms"]
            }
          },
          "response": [
            {
              "name": "201 Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "body": "{\n  \"id\":         \"frm_yau0t67tuMoO2TBSdnt-\",\n  \"publicId\":   \"Crp6zkfAJV\",\n  \"name\":       \"Contact\",\n  \"tags\":       [\"leads\"],\n  \"config\":     { \"...\": \"see Form config reference\" },\n  \"fieldSchema\": { \"fields\": [\"...\"] },\n  \"submitUrl\":  \"/f/Crp6zkfAJV\"\n}"
            }
          ]
        },
        {
          "name": "GET /api/v1/forms/:id",
          "request": {
            "method": "GET",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/forms/{{formId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "forms", "{{formId}}"]
            }
          }
        },
        {
          "name": "PATCH /api/v1/forms/:id",
          "request": {
            "method": "PATCH",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Contact v2\",\n  \"tags\": [\"leads\", \"priority\"]\n}"
            },
            "url": {
              "raw":  "{{baseUrl}}/api/v1/forms/{{formId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "forms", "{{formId}}"]
            },
            "description": "Partial update. Send only the keys you want to change. Pass `{\"archived\": true|false}` to soft-archive / unarchive."
          }
        },
        {
          "name": "DELETE /api/v1/forms/:id",
          "request": {
            "method": "DELETE",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/forms/{{formId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "forms", "{{formId}}"]
            }
          }
        },
        {
          "name": "POST /api/v1/forms/:id/test-render",
          "request": {
            "method": "POST",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sample_data\": {\n    \"email\":   \"preview@example.com\",\n    \"message\": \"Sample for the email preview\"\n  }\n}"
            },
            "url": {
              "raw":  "{{baseUrl}}/api/v1/forms/{{formId}}/test-render",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "forms", "{{formId}}", "test-render"]
            },
            "description": "Renders the email the form would send with `sample_data`. Returns `{source, subject, replyTo, html, derived, conditional}` — does NOT store a submission."
          }
        },
        {
          "name": "POST /api/v1/forms/:id/test-webhook",
          "request": {
            "method": "POST",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sample_data\": {\n    \"email\":   \"preview@example.com\",\n    \"message\": \"Webhook test\"\n  }\n}"
            },
            "url": {
              "raw":  "{{baseUrl}}/api/v1/forms/{{formId}}/test-webhook",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "forms", "{{formId}}", "test-webhook"]
            },
            "description": "Sends a real POST with sample data to the configured webhook URL. Returns `{status, ok, responseBodyExcerpt, payloadSent}`."
          }
        }
      ]
    },

    {
      "name": "Submissions",
      "item": [
        {
          "name": "GET /api/v1/forms/:id/submissions",
          "request": {
            "method": "GET",
            "url": {
              "raw":   "{{baseUrl}}/api/v1/forms/{{formId}}/submissions?limit=50",
              "host":  ["{{baseUrl}}"],
              "path":  ["api", "v1", "forms", "{{formId}}", "submissions"],
              "query": [
                { "key": "from",   "value": "",   "disabled": true, "description": "ISO date" },
                { "key": "to",     "value": "",   "disabled": true, "description": "ISO date" },
                { "key": "status", "value": "",   "disabled": true, "description": "received | spam | invalid" },
                { "key": "limit",  "value": "50", "description": "Max 200, default 50" }
              ]
            }
          }
        },
        {
          "name": "GET /api/v1/forms/:id/submissions.csv",
          "request": {
            "method": "GET",
            "header": [{ "key": "accept", "value": "text/csv" }],
            "url": {
              "raw":  "{{baseUrl}}/api/v1/forms/{{formId}}/submissions.csv",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "forms", "{{formId}}", "submissions.csv"]
            },
            "description": "Pro feature. Streams all submissions as text/csv. 402 csv_export_not_on_plan on Free."
          }
        },
        {
          "name": "GET /api/v1/submissions/:id",
          "request": {
            "method": "GET",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/submissions/{{submissionId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "submissions", "{{submissionId}}"]
            }
          }
        },
        {
          "name": "DELETE /api/v1/submissions/:id",
          "request": {
            "method": "DELETE",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/submissions/{{submissionId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "submissions", "{{submissionId}}"]
            }
          }
        },
        {
          "name": "POST /api/v1/submissions/:id/resend-email",
          "request": {
            "method": "POST",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/submissions/{{submissionId}}/resend-email",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "submissions", "{{submissionId}}", "resend-email"]
            }
          }
        },
        {
          "name": "POST /api/v1/submissions/:id/resend-webhook",
          "request": {
            "method": "POST",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/submissions/{{submissionId}}/resend-webhook",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "submissions", "{{submissionId}}", "resend-webhook"]
            }
          }
        }
      ]
    },

    {
      "name": "Webhooks",
      "item": [
        {
          "name": "GET /api/v1/forms/:id/webhook-deliveries",
          "request": {
            "method": "GET",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/forms/{{formId}}/webhook-deliveries",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "forms", "{{formId}}", "webhook-deliveries"]
            },
            "description": "Last 100 delivery attempts (newest first). Status: success | pending | failed | giving_up."
          }
        }
      ]
    },

    {
      "name": "Emails",
      "item": [
        {
          "name": "GET /api/v1/forms/:id/email-deliveries",
          "request": {
            "method": "GET",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/forms/{{formId}}/email-deliveries",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "forms", "{{formId}}", "email-deliveries"]
            },
            "description": "Last 100 email delivery attempts. Status: success | pending | failed."
          }
        }
      ]
    },

    {
      "name": "API keys",
      "item": [
        {
          "name": "GET /api/v1/apikeys",
          "request": {
            "method": "GET",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/apikeys",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "apikeys"]
            }
          }
        },
        {
          "name": "POST /api/v1/apikeys (account scope)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const j = pm.response.json();",
                  "if (j && j.key) {",
                  "  pm.collectionVariables.set('apiKey', j.key);",
                  "  console.log('apiKey saved to collection variable');",
                  "}",
                  "pm.test('201 created', () => pm.response.to.have.status(201));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"scope\": \"account\",\n  \"name\":  \"postman\"\n}"
            },
            "url": {
              "raw":  "{{baseUrl}}/api/v1/apikeys",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "apikeys"]
            },
            "description": "Creates an account-scope key. The raw `key` is shown ONCE in the response — this test script auto-saves it to the `apiKey` collection variable."
          }
        },
        {
          "name": "POST /api/v1/apikeys (form scope)",
          "request": {
            "method": "POST",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"scope\":  \"form\",\n  \"name\":   \"contact-form\",\n  \"formId\": \"{{formId}}\"\n}"
            },
            "url": {
              "raw":  "{{baseUrl}}/api/v1/apikeys",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "apikeys"]
            }
          }
        },
        {
          "name": "DELETE /api/v1/apikeys/:id",
          "request": {
            "method": "DELETE",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/apikeys/PASTE_KEY_ID",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "apikeys", "PASTE_KEY_ID"]
            }
          }
        }
      ]
    },

    {
      "name": "Billing",
      "item": [
        {
          "name": "GET /api/v1/usage",
          "request": {
            "method": "GET",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/usage",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "usage"]
            },
            "description": "Current period (YYYY-MM) submissions count vs plan limit + limits & features."
          }
        },
        {
          "name": "GET /api/v1/billing/plans",
          "request": {
            "method": "GET",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/billing/plans",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "billing", "plans"]
            }
          }
        },
        {
          "name": "POST /api/v1/billing/checkout",
          "request": {
            "method": "POST",
            "header": [{ "key": "content-type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{ \"plan\": \"pro\" }"
            },
            "url": {
              "raw":  "{{baseUrl}}/api/v1/billing/checkout",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "billing", "checkout"]
            },
            "description": "Returns `{id, url}` — open `url` in a browser to complete the Stripe Checkout."
          }
        },
        {
          "name": "POST /api/v1/billing/portal",
          "request": {
            "method": "POST",
            "url": {
              "raw":  "{{baseUrl}}/api/v1/billing/portal",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "billing", "portal"]
            },
            "description": "Returns the Stripe Billing Portal URL. 400 no_stripe_customer if the user hasn't completed a checkout yet."
          }
        }
      ]
    }
  ]
}
