Skip to content

Campaign Drafts Before Email Verification ​

A signed-in account whose email address is not yet confirmed can save and edit campaign drafts. It cannot publish, submit for review, or edit anything that has left draft. Verifying the email lifts every restriction on this page.

The exception exists so that an autosave never fails: the iOS app saves "New campaign" to the account when the screen is closed, whether or not the address has been confirmed. The website's builder keeps its own draft in the browser (see the guide) and is not affected.

Which requests are allowed ​

POST /fundraisers and PATCH /fundraisers/:id sit behind a draft-aware email gate instead of the plain verified-email gate. For a verified account the gate does nothing. For an unverified account:

RequestAllowed whenOtherwise
POST /fundraisersThe body sends status: "draft" explicitly, and the account holds fewer than 5 live drafts403 EMAIL_NOT_VERIFIED, or 403 UNVERIFIED_DRAFT_LIMIT at the cap
PATCH /fundraisers/:idThe campaign is currently draft, and status is either omitted or "draft"403 EMAIL_NOT_VERIFIED
Anything that publishes (active), submits (pending) or changes another statusNever403 EMAIL_NOT_VERIFIED
Editing a campaign that is no longer a draftNever403 EMAIL_NOT_VERIFIED

An omitted status is not a draft

On create, a body without status is treated as a publish attempt by the gates: the publish checks run as if you had sent active. For an unverified account that is a 403, so always send status: "draft" when saving work in progress.

PATCH /fundraisers/:id is the only route that moves a campaign out of draft — there is no separate publish or submit endpoint — so these two gates are the whole perimeter. Ownership is checked as usual, and an unknown campaign id answers 404.

Cover and gallery uploads (POST /storage/upload, POST /images/save-from-url) were never email-gated, so a draft can carry its photos already.

The draft cap ​

An unverified account may hold at most 5 drafts at once (live rows with status = 'draft', not soft-deleted). The sixth create is refused:

json
{
  "error": "Draft limit reached",
  "message": "You can keep up to 5 drafts until you verify your email address. Verify your email, or delete a draft, to save another.",
  "code": "UNVERIFIED_DRAFT_LIMIT",
  "action": "verify_email",
  "limit": 5
}

The cap is a bound on abuse, not a quota: it is counted before the insert without a lock, so two creates racing at four drafts can both land. Deleting a draft (DELETE /fundraisers/:id) frees a slot. Verifying the email removes the cap.

The refusal body ​

Every email-gate refusal, on these routes and on every other verified-email route, has the same body, so one client handler covers them all:

json
{
  "error": "Email verification required",
  "message": "Please verify your email address to perform this action.",
  "code": "EMAIL_NOT_VERIFIED",
  "action": "verify_email"
}

Key off code, not error. When action is verify_email, offer to resend the verification mail with POST /cognito/resend-verification ({ "email": "…" }, limited to 5 a minute) — see Authentication.

Example: save a draft while unverified ​

bash
curl -X POST https://api.fundlyhub.org/api/v1/fundraisers \
  -H "Authorization: Bearer $FUNDLYHUB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Help the Riverside Library",
    "slug": "help-the-riverside-library",
    "goal_amount_cents": 500000,
    "status": "draft"
  }'

Later edits to the same draft work the same way, as long as they do not send a non-draft status:

bash
curl -X PATCH https://api.fundlyhub.org/api/v1/fundraisers/$ID \
  -H "Authorization: Bearer $FUNDLYHUB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{ "summary": "New shelves and a reading corner for kids" }'

After the email is verified, publish with PATCH and status: "active"; the normal publish gate applies.

Built with VitePress