# Customer-hosted applications

Collect an application, complete required identity checks and recover document uploads safely.

## REST operations and key permissions

| Task | Key permissions | Start here |
| --- | --- | --- |
| Use the customer-server application intake budget or hosted eID handoff | `write:applications` on a separate server key | `POST /api/v1/public/units/{slug}/application-eid/handoffs` |

## Build a customer-hosted application flow

Use the versioned REST intake when you host an application form on your own website. Start from the confirmed API origin in the connection guide. This flow is separate from landlord application reads and from MCP tools; a company key never stands in for the applicant's identity or consent.

### 1. Load the current form before collecting an application.

Read `GET /api/v1/public/units/{slug}/application-form?language=sv` (also `en` or `es`). Keep the returned `unitSlug`, `listingPeriodId` and opaque `formVersion`. Render the published `inputSchema`, conditional `rules`, `policies.confirmations`, document requirements and `identityGate`; do not recreate them from assumptions. Link the supplied privacy/terms paths through the confirmed Homito web-app origin, not the API origin. Check `canApply` and `unavailableReason`; reading a form does not create or reopen an application round.

Use `POST /api/v1/public/units/{slug}/application-form/validate` for draft feedback. Draft validation checks the shared field rules, confirmations and declared document metadata against a read-only form snapshot. It creates no application or consent audit, needs no submission key, verifies no file bytes and starts no identity or screening provider call. The final submission is validated again.

### 2. Keep each credential in its own role.

| Credential | Purpose | Boundary |
| --- | --- | --- |
| `X-Homito-Customer-Key` | A separate server key with `write:applications` for the customer's shared intake budget and hosted eID handoff | Server only; same company as the advertised unit; does not replace applicant capabilities |
| `X-Application-Submission-Key` | One UUID v4 generated before the first submission | Keep the same key and accepted request body/files after a lost response; it is not a landlord `Idempotency-Key` |
| `X-Application-Receipt` | JSON containing `kind: "public"`, the original unit slug as `target`, and the original submission key as `key` | Send in the receipt header, never a URL; it proves only commit existence and attachment state |
| `X-Application-Upload-Key` | A separate capability generated from 32 cryptographically random bytes encoded as 64 lowercase hexadecimal characters | Required for manifest upload operations; a receipt, application ID or company key cannot replace it |

The customer key changes the shared company traffic budget; it does not grant another company's access or replace receipt/upload permissions. An invalid supplied key fails closed instead of falling back to public access. Keep company keys on the server and all capabilities out of public caches, URLs, logs and agent conversations. Preserve them in the visitor's protected form session before the first network request.

### 3. Complete the required identity handoff, then submit once.

When `identityGate.required` is true, use the published hosted eID handoff. An administrator first registers the customer's exact HTTPS callback through the existing authenticated control plane. On the customer server, bind random `state`, a private PKCE verifier and its `S256` challenge to the current visitor's protected form session, the current form/round and the original submission key. Create the handoff at `POST /api/v1/public/units/{slug}/application-eid/handoffs` using the same company's server key and the returned form contract.

Persist the handoff state before redirecting to the returned authorization URL and honor its `expiresAt`. At the registered callback, validate `state` against that same visitor's session; a global state lookup is insufficient. Keep the code/verifier private, suppress callback query logging and redirect immediately to a clean form URL. A key, account, `hasNoPersonalNumber` flag or client assertion cannot bypass required eID.

Submit `POST /api/v1/applications` as `multipart/form-data` with `X-Homito-Application-Contract: 1` and the original `X-Application-Submission-Key`. Send an accurate `Content-Length`; a missing or mismatched length is rejected. Include `unitSlug`, `listingPeriodId`, `formVersion` and the JSON `application` fields. Where required, send the separate `identityHandoff` JSON field containing `handoffId`, `code` and `codeVerifier`; never put it inside `application` or expose the verifier to browser code. For a handoff-backed submission, also send the exact original customer key that created the handoff; another key for the same company cannot substitute. Legacy JSON intake does not provide this versioned contract.

Use the form's `documents.maxExternalBodyBytes` for the whole multipart request. Inline files use the published attachment fields; larger documents use `attachmentManifest` with no inline files and the separate upload key. Follow `documents.manifestSchema`, `maxFiles`, `maxFileBytes`, `allowedMimeTypes` and `acceptedByteFormats`, including the requirements for the main applicant and every co-applicant. A manifest declares intended files; it is not received evidence.

### 4. Read the receipt before deciding what to retry.

Read `GET /api/v1/applications/receipt` with the original receipt header. `received` means the application committed; inspect its separate `attachments` state rather than creating another submission. An identical committed retry returns the original receipt before rechecking a consumed or expired one-use identity proof. `unknown` can mean not sent, still processing or rolled back: use bounded backoff and, when retrying, the identical request with the original key. `removed` is terminal; stop old retries. Receipt reads grant no applicant profile, document download, screening result or decision rights. `notifications: unknown` is not proof of email delivery.

### 5. Upload declared bytes and finalize.

All four operations below require the original `X-Application-Upload-Key`. Preserve every manifest upload ID and the original file bytes/hash across interruption.

| Operation | What it confirms |
| --- | --- |
| `GET /api/v1/applications/{applicationId}/uploads` | Current `finalized` flag and each declared file's `pending`, `received`, `expired` or `stale` state |
| `POST /api/v1/applications/{applicationId}/uploads/{uploadId}/url` | A short-lived PUT URL and exact headers, or an already-`received` result that needs no PUT |
| `POST /api/v1/applications/{applicationId}/uploads/{uploadId}/complete` | Actual bytes, length, SHA-256, byte format/MIME and current subject binding were checked and the attachment committed |
| `POST /api/v1/applications/{applicationId}/finalize` | All declared documents are committed in the ordinary document store; otherwise the operation is refused |

Upload only to the exact returned URL with its exact method, headers and byte length. Never send the company key or applicant capabilities to object storage, follow redirects or log the signed URL. A successful PUT alone is not completion. A customer-server upload proxy avoids assuming that direct browser-to-storage CORS is configured.

After a lost PUT response, try `complete` for the same upload ID before uploading again. On `upload_missing`, obtain another URL for that same ID, upload the original bytes and complete it. Refreshing an expired PUT URL does not renew the underlying upload capability. Stop at expired capability or changed applicant/round scope; changing a file or person requires the explicitly authorized correction/completion flow, not a new submission key or a modified accepted manifest.

### Recover by error code, without creating a second application.

| Failure | Next action |
| --- | --- |
| `eid_required` (403) | Complete the approved hosted identity flow; do not invent a bypass |
| `form_version_changed` / `application_round_changed` (409) | Reload the current contract and ask the applicant to confirm changed information before a new accepted submission; preserve any existing receipt |
| `submission_payload_changed` (409) | Reconcile the original accepted body/files; do not automatically create another operation |
| `upload_subject_changed` (409), stale or expired upload access | Stop upload/finalization and use the authorized completion path for the current subject |
| `submission_removed` (410) | Stop retrying the removed submission |
| File/format validation failure (400/411/413/415/422) | Correct the reported request or document issue before an authorized retry |
| 429, 503 or a lost response | Honor `Retry-After` when supplied, retain the checkpoint and read receipt/progress before an original-key retry |

`application.submitted` and `application.intake_finalized` are separate webhook events. Verify/deduplicate them as described in the webhook guide and use receipt/progress for current state. Neither an event nor finalization asserts email delivery, a rental decision or a completed screening. Keep applicant data and upload/receipt capabilities outside public listing caches.

## Keep private data out of public caches

Keep personal data, receipts and upload capabilities out of public listing caches. Tenant and application reads intentionally return minimized operational projections; do not infer missing private fields or screening conclusions. Keep nullable/unknown values distinct from zero or a completed result.
