Gamma Duo Partner Application API
Submit loan applications collected on your own website, form or system into the Gamma Duo platform. Each submission is authenticated, validated, mapped to our lender's schema and forwarded in real time.
- Protocol: HTTPS, JSON,
POST - Auth: per-partner API key in the
X-API-Keyheader - Integration style: server-to-server (do not call this from a browser: your key must never reach a client device)
- Version:
v1(breaking changes ship under a new path, e.g./api/v2/…)
Prefer to let applicants fill in our form on your website instead of building your own? See the Form & Embed guide: a drop-in
<script>widget that submits through the same pipeline.
1. Environments & base URLs
| Environment | Base URL |
|---|---|
| UAT / testing | https://<uat-deployment-host> |
| Production | https://<prod-deployment-host> |
We provide your exact host(s) at onboarding. All examples below use
$BASE_URL. The current upstream lender environment is Nimo UAT.
Health probe (no auth): GET $BASE_URL/api/v1/health →
{
"ok": true,
"service": "gamma-duo-intake",
"time": "2026-07-13T05:00:00.000Z",
"nimo": "configured",
"db": "configured",
"submitToNimo": true,
"partners": 1
}
The response carries additional operational fields; the two that matter for
integrators are nimo ("simulated" means lender credentials aren't
configured yet; see §6) and partners (how many partner keys are active).
?deep=1 additionally verifies database connectivity.
2. Authentication
Send your key on every request:
X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx
- Keys are issued per partner and identify your organisation; every application
you submit is attributed to your
partnerId. - Applications created through the partner API are recorded as
broker-originated (channel
broker) in our system - they appear that way in our ops console and reporting, alongside yourpartnerId. - Every application you send is also labelled "Referred by" with your registered organisation name, taken from your key. It is derived from the authenticated key on our side, not from the payload, so there is nothing to send for it - and no caller can submit under another partner's name. Tell us if the name we show should change.
- API keys are for server-to-server integrations. Human brokers who want to sign in, start applications and track their pipeline use the broker portal instead (invited by the Gamma Duo team) - ask us if that fits your workflow better. The two can coexist for one brokerage.
- Store the key as a server-side secret (env var / secret manager). Never commit it or expose it in front-end code.
- To rotate or revoke a key, contact us; we can run old and new keys in parallel during a cutover.
Missing/invalid keys are rejected before any processing (see §5).
3. Endpoint
POST $BASE_URL/api/v1/partner-applications
Content-Type: application/json
X-API-Key: <your key>
Request envelope:
{
"externalApplicationId": "your-ref-99881", // optional: your own reference, echoed back
"application": { /* canonical application, see §4 */ }
}
3.1 Hand-off drafts (server-side prefill)
If you don't hold a complete application - you captured some details and want the applicant to finish the rest themselves - create a hand-off draft instead of a submission:
POST $BASE_URL/api/v1/partner-drafts
Content-Type: application/json
X-API-Key: <your key>
Same envelope as §3, but every application field is optional (send what
you have; unknown fields are ignored). The only requirement is someone to hand
off to: at least one of applicant.firstName, applicant.email or
applicant.phone, else 422 nothing_to_prefill.
{
"externalApplicationId": "your-ref-99881",
"application": {
"product": "car",
"applicant": { "firstName": "Ana", "lastName": "Reyes", "email": "[email protected]", "phone": "0400 999 888" },
"loan": { "amount": 18000, "termYears": 5 }
}
}
200 response:
{
"applicationId": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f",
"resumeToken": "rt_9kq…",
"resumeUrl": "https://apply.gammaduo.com.au/?resume=c3d4e5f6-…&rt=rt_9kq…",
"status": "lead",
"partnerId": "broker_123",
"externalApplicationId": "your-ref-99881"
}
Send the applicant to resumeUrl (email/SMS it, or redirect them): the
form opens with everything you supplied already filled in, and they complete
the remaining steps and submit. It's one application file end to end - your
partnerId/externalApplicationId stay attached through final submission,
and our operations team sees the draft immediately.
Notes:
- The resume link is a credential. Anyone with the URL can open (and continue) that draft - deliver it to the applicant privately, don't post it anywhere public. We store only a hash of the token.
resumeUrlisnullif the hosted form's base URL isn't configured for the environment; the recipe is{form base}/?resume={applicationId}&rt={resumeToken}(add&product=personalfor personal loans).- Consents and declarations can never be prefilled -
consents, repayment-issue declarations and bank-statement fields in the payload are ignored. The applicant answers those in the form. - Idempotent on
externalApplicationId. Re-posting the sameexternalApplicationIdreturns the same application (the response carries"existing": true) instead of creating another one - so retries and double-fires are harmless. Each repeat rotates the resume link: the newestresumeUrlis the live one and earlier links stop working, so always deliver the link from your latest response. While the applicant hasn't opened the form yet, a repeat also refreshes the prefill with whatever you send; once they've started, their answers are kept and only the link rotates. If the application has already been submitted, a repeat returns409 application_already_submitted(with theapplicationId). - Omitting
externalApplicationIddisables this: every call then creates a separate draft. Always send your reference. - Drafts follow the platform's data-retention window: an abandoned draft is eventually anonymised, after which the link stops working.
- Prefer this over the client-side
?prefill=links in the embed guide when you have an API key: nothing travels in the applicant's URL, and the lead exists on our side even if they never open the link.
4. The application object (canonical schema)
Field names below are exact. Unknown fields are ignored. Send what you have; only the fields marked required must be present.
Note: fields that exist only in our broker portal's version of the form (e.g. the broker's own reference and authority declaration) are not part of this schema - use
externalApplicationIdfor your reference.
4.1 Top level
| Field | Type | Notes |
|---|---|---|
product |
string | required: "car" or "personal". ("commercial" asset finance exists on our platform but has no lender integration yet - sending it captures the application for manual processing instead of lodging it; treat it as unsupported here until announced.) |
applicant |
object | required, see §4.2 |
loan |
object | required, see §4.3 |
consents |
object | required, see §4.6 |
vehicle |
object | car loans, see §4.4 |
employment |
object | see §4.5 |
expenses |
object | monthly amounts (AUD), see §4.7 |
assets |
array | see §4.8 |
liabilities |
array | see §4.8 |
4.2 applicant
| Field | Type | Req | Notes |
|---|---|---|---|
firstName |
string | ✅ | min 2 chars |
lastName |
string | ✅ | min 2 chars |
dateOfBirth |
string | ✅ | YYYY-MM-DD, applicant must be 18 to 120 |
email |
string | ✅ | valid email |
phone |
string | ✅ | AU mobile recommended, e.g. 0411223344 (≥ 8 digits) |
address |
string | ✅ | single-line residential address (see note) |
previousAddress |
string | supply if < 3 years at current address | |
postalSameAsResidential |
boolean | default true |
|
postalAddress |
string | required when postalSameAsResidential is false |
|
maritalStatus |
string | SINGLE | MARRIED | DEFACTO | DIVORCED | WIDOWED |
|
residentialStatus |
string | OWN_MORTGAGE | OWN_NO_MORTGAGE | RENTING | BOARDING | WITH_PARENTS | CARAVAN | OTHER |
|
residencyStatus |
string | AU_CITIZEN | AU_PERMANENT_RESIDENT | AU_TAX_RESIDENT | NON_AU_RESIDENT |
|
dependents |
integer | number of dependants | |
yearsAtAddress |
integer | years at current address | |
isExistingCustomer |
boolean | default false |
Address note: send a single line as
"<unit>/<number> <street>, <suburb> <STATE> <postcode>"(e.g."8/210 Coronation Drive, Milton QLD 4064"). We parse it into structured components best-effort; the full line is always preserved.
4.3 loan
| Field | Type | Req | Notes |
|---|---|---|---|
amount |
number | ✅ | > 0. Net amount to finance. Personal loans: the net amount (after any deposit) must be $8,000-$60,000, the lender product's pricing range - outside it the submission is rejected with a validation error |
purpose |
string | ✅ for personal |
DEBT_CONSOLIDATION | HOME_IMPROVEMENT | TRAVEL | WEDDING | MEDICAL | MAJOR_PURCHASE | EDUCATION | OTHER |
purposeDetail |
string | What the funds will be used for, in the applicant's words (up to 300 chars). Strongly recommended when purpose is OTHER or MAJOR_PURCHASE - the assessment team needs the true purpose. The hosted form requires it for both; the partner API does not, so existing integrations keep working |
|
deposit |
number | car loans: deposit / trade-in | |
termYears |
number | preferred term; feeds the lender's product selection (defaults to 5 when omitted) | |
repaymentFrequency |
string | weekly | fortnightly | monthly; feeds the lender's product selection (defaults to monthly) |
4.4 vehicle (car loans)
| Field | Type | Notes |
|---|---|---|
found |
boolean | true if a specific vehicle is identified |
rego |
string | registration plate |
state |
string | NSW | VIC | QLD | SA | WA | TAS | ACT | NT |
make, model |
string | |
year |
string | year of manufacture, e.g. "2022" |
vin |
string | |
bodyType |
string | free text, e.g. Sedan, SUV, Ute |
value |
number | estimated / purchase value |
isNew |
boolean | true new, false used |
4.5 employment
| Field | Type | Notes |
|---|---|---|
type |
string | PAYG | CASUAL | CONTRACT | SELF_EMPLOYED | RETIRED | UNEMPLOYED |
time |
string | FULL_TIME | PART_TIME | CASUAL. The lender requires a basis whenever type is PAYG/SELF_EMPLOYED - if omitted we submit FULL_TIME |
employer |
string | |
employerContactNumber |
string | required unless type is RETIRED/UNEMPLOYED (min 8 digits). The lender uses it to verify employment |
occupation |
string | |
startDate |
string | YYYY-MM or YYYY-MM-DD |
onProbation |
boolean | defaults to false when omitted |
grossIncome |
number | |
incomePeriod |
string | WEEKLY | FORTNIGHTLY | MONTHLY | ANNUALLY |
averageAnnualIncome |
number | required when type is SELF_EMPLOYED - taxable income from the business averaged over the last two years. The lender assesses a business on this rather than a single year |
businessAddress |
string | where the business trades from, single line. Defaults to the applicant's residential address |
businessAddressSameAsResidential |
boolean | default true |
previousEmployer |
string | supply when the business has traded for under 3 years - the lender asks what came before it |
previousEmploymentStartDate |
string | YYYY-MM or YYYY-MM-DD |
Self-employed applicants need the
businessobject as well (§4.9). The lender treats a self-employed income record as a business record and rejects the submission without the ABN, the entity type, the trading-since date and the two-year average.
4.9 business (self-employed applicants, and asset finance)
Required whenever employment.type is SELF_EMPLOYED. Asset finance uses the
same object for the entity being financed, so one shape serves both.
| Field | Type | Req | Notes |
|---|---|---|---|
abn |
string | ✅ | 11 digits. Spaces are fine - we strip them |
entityName |
string | ✅ | the business name (max 50 chars reaches the lender) |
entityType |
string | ✅ | SOLE_TRADER | COMPANY | PARTNERSHIP | TRUST |
abnActiveSince |
string | ✅ | YYYY-MM-DD - when the ABN became active. Under 3 years ago, also send employment.previousEmployer |
abnVerified |
boolean | true if you checked it against the Australian Business Register |
|
abnStatus |
string | what the register said, e.g. Active |
|
abnAgeMonths |
integer | months since abnActiveSince |
|
gstRegistered |
boolean |
Our own form fills all of this from one ABN look-up; if you already hold the ABN, sending it plus the name, type and start date is enough.
4.6 consents
| Field | Type | Req | Notes |
|---|---|---|---|
creditCheck |
boolean | ✅ | must be true |
privacyPolicy |
boolean | ✅ | must be true - the Privacy Policy & Consent |
dataExchange |
boolean | ✅ | must be true - the Consent to the Exchange of Data |
creditGuide |
boolean | ✅ | must be true - applicant confirms they have read the Credit Guide |
marketing |
boolean | optional: marketing communications consent | |
contactOptIn |
boolean | optional: service messaging about this application (progress / resume emails) |
You must show the applicant the disclosure documents and obtain their consent before submitting: the Privacy Policy & Consent, the Consent to the Exchange of Data and the Credit Guide are all published at the links above on the application host. Submissions without all four required consents set to
trueare rejected (422). Hand-off drafts (§3.1) are different: consents can never be preset there - the applicant ticks them in the form.
4.7 expenses (monthly AUD)
Object with any of these numeric keys (note the subscriptionTv casing):
rent, utilities, telecommunications, insurance, groceries, diningOut,
vehiclesTransport, subscriptionTv, entertainment, educationChildcare.
4.8 assets / liabilities
"assets": [
{ "type": "SAVINGS", "value": 15000 }, // PROPERTY|VEHICLE|SHARES|SUPERANNUATION|SAVINGS|OTHER
{ "type": "PROPERTY", "value": 700000, "purpose": "LIVE_IN", "propertyType": "HOUSE" },
{ "type": "OTHER", "description": "Boat", "value": 9000 }
],
"liabilities": [
{ "type": "CREDIT_CARD", "lender": "ANZ", "balance": 2000, "monthlyRepayment": 100, "limit": 15000 }
// type: HOME_LOAN|CAR_LOAN|PERSONAL_LOAN|CREDIT_CARD|BNPL|SALARY_ADVANCE|OTHER
]
Property assets: the lender requires purpose (LIVE_IN | INVESTMENT) and
propertyType (HOUSE | APARTMENT_UNIT | TOWNHOUSE_VILLA |
RESIDENTIAL_VACANT_LAND | COMMERCIAL_PROPERTY) on every PROPERTY asset.
Send them when you know them; if omitted we derive them (a homeowner's first
property is treated as LIVE_IN, anything else as INVESTMENT, type
HOUSE). Liabilities: the lender requires a credit limit on every
liability; if omitted we submit the balance as the limit.
5. Examples
Copy-paste, tested payloads live in docs/examples/ and are
verified by our test suite on every change:
partner-application.minimal.json: required fields onlypartner-application.full.json: a complete applicationpartner-application.self-employed.json: a self-employed applicant, with thebusinessobject the lender requires
Minimal request:
{
"externalApplicationId": "acme-form-10231",
"application": {
"product": "personal",
"applicant": {
"firstName": "Sam", "lastName": "Lee", "dateOfBirth": "1988-07-01",
"email": "[email protected]", "phone": "0400111222",
"address": "44 Smith Street, Collingwood VIC 3066"
},
"loan": { "amount": 15000, "purpose": "DEBT_CONSOLIDATION" },
"consents": { "creditCheck": true, "privacyPolicy": true }
}
}
cURL:
curl -sS -X POST "$BASE_URL/api/v1/partner-applications" \
-H "Content-Type: application/json" \
-H "X-API-Key: $PARTNER_API_KEY" \
--data @docs/examples/partner-application.full.json
6. Responses
200: accepted & submitted
{
"applicationId": "3f9d2c80-5b7e-4c1a-9f0a-2d6b8e4a1c55",
"reference": "GGD-411394",
"status": "submitted",
"externalApplicationId": "acme-form-99881",
"partnerId": "broker_123",
"nimo": { "message": "Application Created Successfully" }
}
reference: quote this to the applicant.applicationId: a UUID; store it against your record for support/correlation.- The application is also recorded on the Gamma Duo platform: our operations
team sees it (with your
partnerIdandexternalApplicationId) and can assist the applicant from there. - In UAT before credentials are live, success responses also carry
"simulated": trueand nothing is sent to the lender. Treat asimulatedresponse as accepted for testing, not a real lodgement.
Error responses
All errors are JSON with an error code. Full catalogue:
| HTTP | error |
When | Retry? |
|---|---|---|---|
| 400 | invalid_json |
body isn't valid JSON | No, fix the request |
| 400 | missing_application |
no application object. If the canonical fields are at the top level instead, the message says so and foundAtTopLevel[] lists them - see below |
No, fix the request |
| 401 | unauthorized |
missing or invalid X-API-Key (message says which) |
No, fix the key |
| 503 | not_configured |
partner programme not enabled on this host | No, contact us |
| 405 | method_not_allowed |
not a POST |
No |
| 422 | validation_failed |
see fields[] |
No, fix and resubmit |
| 422 | nothing_to_prefill |
hand-off drafts (§3.1): no applicant.firstName/email/phone supplied |
No, fix the request |
| 409 | application_already_submitted |
hand-off drafts (§3.1): that externalApplicationId already completed an application (applicationId echoed) |
No |
| 429 | rate_limited |
too many requests from one address (retryAfterSec in the body, Retry-After header) |
Yes, after Retry-After |
| 424 | nimo_rejected |
lender rejected it (nimoStatus, nimo echo the reason) |
Only after fixing the flagged data |
| 424 | nimo_auth_failed |
our lender credentials were rejected (server config), not your fault | No, contact us |
| 424 | nimo_unreachable |
lender network/timeout error (detail) |
Yes, with backoff |
The most common integration mistake is sending the fields flat, without
the application wrapper:
// WRONG - fields at the top level
{ "product": "personal", "applicant": { … }, "loan": { … } }
// RIGHT - wrapped, with externalApplicationId BESIDE it
{ "externalApplicationId": "your-ref", "application": { "product": "personal", "applicant": { … }, "loan": { … } } }
The error names it rather than just saying a field is missing:
{
"error": "missing_application",
"message": "Wrap the application fields in an 'application' object: {\"application\": { … }}. Found 'product', 'applicant', 'loan' at the top level instead. 'externalApplicationId' stays beside 'application', not inside it (…).",
"foundAtTopLevel": ["product", "applicant", "loan"]
}
422 shape (one entry per invalid field):
{
"error": "validation_failed",
"fields": [
{ "field": "applicant.dateOfBirth", "message": "required, format YYYY-MM-DD" },
{ "field": "consents.creditCheck", "message": "must be true" }
]
}
Fields validated: product, applicant.firstName, applicant.lastName,
applicant.email, applicant.phone, applicant.address,
applicant.dateOfBirth (18+), loan.amount (> 0; personal loans: net
$8,000-$60,000), loan.purpose
(personal loans), consents.creditCheck, consents.privacyPolicy,
consents.dataExchange, consents.creditGuide,
employment.employerContactNumber (min 8 digits; required whenever
employment.type is a working type, i.e. anything other than
RETIRED/UNEMPLOYED, because the lender uses it to verify employment), and -
when employment.type is SELF_EMPLOYED - business.abn,
business.entityName, business.entityType, business.abnActiveSince and
employment.averageAnnualIncome.
424 nimo_rejected shape:
{ "error": "nimo_rejected", "nimoStatus": 400, "nimo": { "message": "Bad request", "errors": { "error": "…reason…" } } }
7. Idempotency, retries & rate limits
- Submission (
/partner-applications) is not yet idempotent. A retried request creates a new application, so only retry on a non-2xx response (network error,424 nimo_unreachable, timeout). Never retry a200. - Hand-off drafts (
/partner-drafts) ARE idempotent when you sendexternalApplicationId- retries return the same application (see §3.1). - Use exponential backoff (e.g. 2s, 4s, 8s) for
nimo_unreachable, and honour theRetry-Afterheader on a429. - Requests are throttled per source address. The limits sit far above any legitimate submission volume; if you expect sustained bursts of more than about one request per second, talk to us first and we'll size for it.
- The
Idempotency-Keyheader is reserved for a future release; sending it today is accepted but has no effect.
8. Limitations (current build)
- Synchronous: the response reflects the round-trip to the lender. Set a client timeout of ~30s.
- Single applicant: joint applications (co-applicants) aren't supported yet; a co-applicant object would be ignored.
- No idempotency yet. Planned; this document will be versioned when it lands (see §7 for safe retry rules meanwhile).
- No status callbacks yet: the synchronous response is the only signal.
There is no webhook for later status changes; quote the
reference/applicationIdwhen following up with us.
9. Support
Contact your Gamma Duo integration contact for onboarding, a UAT key, host URLs,
and to report issues (include applicationId and externalApplicationId).