# Gamma Duo Form & Embed Guide

Put the Gamma Duo loan application form on any website with one `<script>` tag,
or link applicants to our hosted form. Either way, applications flow through
the same validated pipeline as the [Partner API](/docs) and land with our
operations team (and, on final submission, the lender).

- **Hosted form:** `$BASE_URL/` (link to it directly, or iframe it).
- **Embeddable widget:** `$BASE_URL/embed/gamma-duo-form.iife.js`, a
  standalone bundle (React included, styles self-injected). No dependencies,
  no build step, no API key.

---

## 1. Quick start

**Script tag (any website)**

```html
<div id="loan-form"></div>
<script src="$BASE_URL/embed/gamma-duo-form.iife.js"></script>
<script>
  GammaDuoForm.mount("#loan-form", { product: "car" });
</script>
```

**Declarative (auto-mounts on load)**

```html
<div data-gamma-duo-form='{"product":"personal"}'></div>
<script src="$BASE_URL/embed/gamma-duo-form.iife.js"></script>
```

**React app**

```jsx
import { GammaDuoForm } from "gamma-duo-form";

<GammaDuoForm product="car" submitUrl="$BASE_URL/api/v1/applications" />
```

When embedding on a domain other than ours, always pass `submitUrl` (see §4);
the widget derives its other endpoints from it.

---

## 2. Options

| Option | Default | Notes |
| --- | --- | --- |
| `product` | `"car"` | `"car"` or `"personal"` |
| `direction` | `"sectioned"` | layout style: `"sectioned"` · `"flow"` · `"focus"` |
| `accent` | `"#E8476A"` | brand accent colour (any CSS colour) |
| `font` | `"Plus Jakarta Sans / Inter"` | also `"Sora / Inter"`, `"Manrope"` |
| `mascot` | `"milestones"` | `"off"` · `"milestones"` · `"always"` |
| `height` | `"auto"` | `"auto"` (flows in page), `"fill"`, `"640px"`, or a px number |
| `persist` | `true` | saves progress in the browser so applicants can resume |
| `initialValues` | (none) | object of form fields to prefill (same vocabulary as §3's hand-off links) |
| `logoUrl` / `mascotUrl` | (none) | white-label the brand assets |
| `submitUrl` | `/api/v1/applications` | **required for cross-origin embeds**: full URL of our intake endpoint |
| `attribution` | auto | captured from the host page URL (`?refsource=`, `utm_*`); pass an object to override or `false` to disable |
| `onStepComplete(stepId, form)` / `onChange(form)` | (none) | host page hooks (analytics etc.) |

The layout responds to the **container** width, not just the viewport, so the
form renders correctly inside a narrow column.

**Attribution:** add `?refsource=your_id` to any page that hosts the widget
(or to links pointing at our hosted form) and every application from that
visit is tagged with your referral source; first touch persists across
return visits. Note this is *claimed* attribution; if you need verified,
per-application attribution and server-side control, use the
[Partner API](/docs) instead.

---

## 3. Prefilled hand-off links (traffic partners)

If you drive applicants to our hosted form, you can hand over everything you
already know about them so they start at Step 1 with those answers filled in.

> **Have an API key?** Prefer the server-side hand-off: `POST
> /api/v1/partner-drafts` (Partner API guide, §3.1) stores what you send and
> returns a short resume link for the applicant. Nothing travels in the URL,
> and the lead exists on our side even if they never open the link. The
> `?prefill=` links below need no credentials, but the data rides in the
> link itself.

Build the link:

```
$BASE_URL/?refsource=YOUR_PARTNER_ID&prefill=<encoded JSON>
```

- **`refsource`** (required for attribution) - your agreed partner identifier.
  Every application from the visit is tagged with it, and the first touch
  persists even if the applicant returns later with a clean URL.
- **`prefill`** - a JSON object of form fields, encoded either way:
  - URI-encoded JSON: `encodeURIComponent(JSON.stringify(data))`
  - base64url of the UTF-8 JSON (shorter links, no `%` soup)
- **`product`** (optional) - `car` (default) or `personal`.

```js
// Building a hand-off link (browser or Node 18+)
const data = {
  firstName: "Jane", lastName: "Smith", email: "jane@example.com",
  phone: "0411 223 344", dateOfBirth: "1990-04-15",
  address: "12 Smith Street, Richmond VIC 3121",
  loanAmount: 25000, depositAmount: 5000,
};
const b64 = btoa(String.fromCharCode(...new TextEncoder().encode(JSON.stringify(data))))
  .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const url = `$BASE_URL/?refsource=YOUR_PARTNER_ID&prefill=${b64}`;
```

### Fields you can prefill

Any real form field is accepted; unknown keys are silently ignored. The
useful set:

| Field | Type / values |
| --- | --- |
| `firstName` / `lastName` | string |
| `dateOfBirth` | `YYYY-MM-DD` |
| `email` | string |
| `phone` | Australian mobile, `04########` (spaces ok) |
| `address` | single line, e.g. `"12 Smith Street, Richmond VIC 3121"` |
| `previousAddress` | single line (if under 3 years at current) |
| `yearsAtAddress` | number |
| `residentialStatus` | `OWN_MORTGAGE` · `OWN_NO_MORTGAGE` · `RENTING` · `BOARDING` · `WITH_PARENTS` |
| `maritalStatus` | `SINGLE` · `MARRIED` · `DEFACTO` · `DIVORCED` · `WIDOWED` |
| `residencyStatus` | `AU_CITIZEN` · `AU_PERMANENT_RESIDENT` · `AU_TAX_RESIDENT` · `NON_AU_RESIDENT` |
| `numberOfDependents` | number |
| `loanAmount` / `depositAmount` | number (AUD) |
| `loanTerm` | years, 1-7 |
| `paymentFrequency` | `weekly` · `fortnightly` · `monthly` |
| `loanPurpose` | personal loans: `DEBT_CONSOLIDATION` · `HOME_IMPROVEMENT` · `TRAVEL` · `WEDDING` · `MEDICAL` · `MAJOR_PURCHASE` · `EDUCATION` · `OTHER` |
| `employmentType` | `PAYG` · `CASUAL` · `CONTRACT` · `SELF_EMPLOYED` · `RETIRED` · `UNEMPLOYED` |
| `employmentTime` | `FULL_TIME` · `PART_TIME` · `CASUAL` (basis, when employed) |
| `employerCompanyName` / `occupationName` | string |
| `employerContactNumber` | string, min 8 digits |
| `grossSalary` | number |
| `incomePeriod` | `WEEKLY` · `FORTNIGHTLY` · `MONTHLY` · `ANNUALLY` |
| `vehicleFound` | boolean - with `rego` (string) if known |

### Behaviour and privacy rules

- The applicant lands on **Step 1** and walks the full journey; every
  prefilled value is visible, editable and re-validated as they go.
- **Consents and declarations can never be preset** by a link (credit check,
  privacy, contact opt-in, hardship declaration) - the applicant answers
  those personally. Internal keys are ignored too.
- The `prefill` parameter is **stripped from the URL immediately** after
  load, so personal details don't sit in the address bar, history, bookmarks
  or analytics page-view URLs.
- If the applicant has been here before, **their own saved answers beat the
  link's values** - a hand-off never overwrites what someone typed.
- Always send applicants over **HTTPS**, and only include details you
  actually hold - less is better in a URL.
- This is convenience prefill with *claimed* attribution. If you need
  authenticated, server-attributed submission instead, use the
  [Partner API](/docs).

---

## 4. Cross-origin embedding

The widget talks to three endpoints, all derived from `submitUrl`:

| Purpose | Derived endpoint |
| --- | --- |
| Final submission | `submitUrl` (e.g. `$BASE_URL/api/v1/applications`) |
| Bank-statements session (Step 6) | `…/api/v1/bank-session` |
| Server-side draft saves / resume | `…/api/v1/drafts` |

CORS is open by default and can be locked to specific origins on our side;
tell us the domains you'll embed on and we'll allowlist them.

These endpoints carry per-visitor rate limits sized well above real applicant
behaviour. Because the widget calls them from each applicant's own browser,
embedding sites never share a limit; a rate-limited response (`429`) simply
retries after the `Retry-After` interval.

---

## 5. The form's endpoints (reference)

These are the public endpoints behind the widget. They serve the anonymous
form, so they require no API key. Server-to-server integrators should use the
[Partner API](/docs) instead, which authenticates you and attributes
applications to your organisation.

### `POST /api/v1/applications` (final submission)

```jsonc
{
  "applicationId": "3f9d2c80-…",   // optional UUID; reused across the whole journey
  "product": "car",                 // or "personal"
  "form": { /* the widget's form state */ },
  "attribution": { "refsource": "…", "utm_source": "…" }  // optional
}
```

- **200** `{ applicationId, reference: "GGD-######", status: "submitted", … }`:
  the application is recorded on our platform. Depending on platform
  configuration it is either forwarded to the lender immediately (the response
  then includes the lender acknowledgement under `nimo`) or captured for
  review, in which case the response also carries `"deferred": true` and our
  operations team completes the lodgement.
- **422** `{ error: "validation_failed", fields: [{ field, message }] }`:
  same validation rules as the Partner API (see [§6 there](/docs)), including
  consents and, for working applicants, the employer contact number.
- **424** `nimo_rejected` / `nimo_auth_failed` / `nimo_unreachable`: lender
  round-trip problems, same catalogue as the Partner API.

### `POST /api/v1/bank-session` (Step 6, bank statements)

Called when the applicant reaches the income-verification step. Validates the
data captured so far (consents and employment are not yet required at this
point), registers the application with the lender, and returns
`{ nimoReference, bankStatementsUrl }`; `bankStatementsUrl` is what the widget
opens full-screen for illion bank-statement sharing. Bank credentials are
entered on illion's hosted page only; they never touch Gamma Duo or your site.

`GET /api/v1/bank-session?id=<applicationId>` returns `{ completed: boolean }`
- true once illion has delivered the statements data back to us. The widget
polls this while the statements screen is open and closes it automatically on
completion; no integration work is needed on your side.

### `POST /api/v1/drafts` · `GET /api/v1/drafts?id=…&token=…`

Progressive server-side draft capture. The first save (after Step 1) creates
the application and returns a `resumeToken`; the widget stores it and sends it
with every later save. `GET` returns the draft for resume; this is what our
"finish your application" email links use
(`$BASE_URL/?resume=<applicationId>&rt=<token>`). Tokens are single-purpose,
hashed at rest and expiring; a link that has expired simply starts a fresh
application.

### `GET /api/v1/opt-out?id=…&token=…`

One-click unsubscribe page linked from application emails. Stops service
emails for that application without affecting the application itself.

### `GET /api/v1/health`

No auth. Returns `{ ok: true, service: "gamma-duo-intake", nimo: "configured", … }`.
`nimo: "simulated"` means the lender connection isn't configured in that
environment (submissions complete end-to-end but nothing is lodged). Add
`?deep=1` to also verify database connectivity.

---

## 6. The applicant journey (what the widget does)

1. **Steps 1-5:** details, vehicle/purpose, situation, ID, loan term. Progress
   is saved locally and (from Step 1) as a server-side draft, so applicants can
   leave and resume.
2. **Step 6:** income verification via illion bank statements (full-screen,
   with a manual "I've finished" fallback).
3. **Steps 7-8:** income, expenses, assets & liabilities. Working applicants
   must provide an employer contact number (the lender phone-verifies
   employment).
4. **Step 9:** review, credit-check + privacy consents, submit. The applicant
   sees a reference (`GGD-######`) and a thank-you screen.

Abandoned applications receive a recovery email with a secure resume link
(where the applicant opted in to contact), and are eventually purged under our
data-retention policy.

---

## 7. Support

Contact your Gamma Duo integration representative for embed onboarding, CORS
allowlisting, custom branding, or a walkthrough of the hosted form. For
server-to-server submission, see the [Partner API](/docs).
