Skip to content

Signup Credit

New IndoxHub accounts can claim a one-time $1 free credit after verifying a payment method. There is no charge — Stripe captures the card via a SetupIntent (zero-amount authorization) so we know the card is real before granting funds.

Eligibility

A user is eligible for the signup credit if all of the following are true:

Gate Source
SIGNUP_CREDIT_ENABLED=true on the backend .env setting (default true)
User's email has been verified via the email-confirmation link users.email_verified = TRUE (set on /auth/confirm)
User has not previously received a signup credit No row in billing_transactions with payment_method='signup_credit' for that user

OAuth signups (Google / GitHub / Apple) bypass the email-confirmation step and currently do not auto-flip email_verified. See the OAuth note below.

Endpoints

GET /api/v1/payments/signup-credit/status

Authenticated. Returns whether the current user can / has claimed the signup credit. Use this to show or hide the "Claim $1" CTA in the frontend.

Response — eligible user, no grant yet:

{
  "eligible": true,
  "granted": false,
  "email_verified": true,
  "amount_usd": 1.0,
  "reason": null
}

Response — already granted:

{
  "eligible": false,
  "granted": true,
  "email_verified": true,
  "amount_usd": 1.0,
  "reason": "already_granted"
}

The reason field is one of null, "disabled", "already_granted", or "email_not_verified".

POST /api/v1/payments/signup-credit/setup-intent

Authenticated. Creates a Stripe Customer (if not already created), persists users.stripe_customer_id, and returns a SetupIntent client_secret the frontend can confirm via Stripe.js.

Response (200):

{
  "client_secret": "seti_…_secret_…",
  "setup_intent_id": "seti_…",
  "customer_id": "cus_…",
  "publishable_key": "pk_live_…"
}

Errors:

Code Reason
401 No valid auth cookie / token
403 SIGNUP_CREDIT_ENABLED=false or user's email is not verified
409 User has already received a signup credit
503 STRIPE_API_KEY not configured on the backend

End-to-end flow

1. User registers          POST /auth/register     → account created, email_verified=FALSE
2. Email confirmation      POST /auth/confirm      → email_verified=TRUE
3. Frontend "Claim $1"     POST /payments/signup-credit/setup-intent
4. Stripe.js confirms card → Stripe fires `setup_intent.succeeded` webhook
5. Webhook handler         → grant_signup_credit(user_id, setup_intent_id, $1)
6. User's account          credits 0 → 1.00, account_tier free → standard

The webhook handler is idempotent:

  • Same setup_intent_id fired twice → only one grant
  • Different setup_intent_id for the same user → still only one grant (one-per-user check)

Configuration

Env var Default Purpose
SIGNUP_CREDIT_ENABLED true Master kill-switch. Set to false to disable the entire flow.
SIGNUP_CREDIT_AMOUNT_USD 1.0 The dollar amount granted.
STRIPE_API_KEY required Stripe secret key (test or live).
STRIPE_PUBLISHABLE_KEY empty Returned to the frontend so Stripe.js can initialize.
STRIPE_WEBHOOK_SECRET required Validates incoming setup_intent.succeeded webhooks.

The Stripe Dashboard webhook endpoint must be subscribed to the setup_intent.succeeded event. The handler ignores SetupIntents whose metadata purpose != "signup_credit", so other SetupIntent flows in the account are unaffected.

OAuth signups (current behavior)

OAuth users (Google / GitHub / Apple) skip the email-confirmation step, which means email_verified stays FALSE and they cannot currently claim the signup credit. The next iteration will mark these users as email_verified=TRUE automatically — the OAuth provider has already verified the email.

Database changes

Migration 015_add_stripe_customer_and_email_verified.sql adds:

  • users.stripe_customer_id VARCHAR(255) UNIQUE — links a user to their Stripe Customer record
  • users.email_verified BOOLEAN NOT NULL DEFAULT FALSE — the eligibility gate
  • users.email_verified_at TIMESTAMP WITH TIME ZONE — when the user confirmed
  • Index idx_users_stripe_customer_id on users(stripe_customer_id)

Backfilling email_verified=TRUE for existing active users is not done automatically. Run a migration script if you want existing users to be eligible.

Documentation last built on May 23, 2026