# Apparently Bank API: embed the apply widget

Human-readable docs: https://apparently.cc/banks/developers. OpenAPI 3.1: https://apparently.cc/api/v1/bank/openapi.json.

## Prompt for your coding agent

Paste the block below into Claude Code, Codex, Cursor, Copilot or any other coding agent, run from the root of your onboarding app.

```text
Integrate the Apparently apply widget into this codebase's merchant onboarding flow.

Apparently reviews a gaming merchant's legal opinion for us (the bank or payment processor). The widget is a white-labelled iframe that shows our brand with a small "Powered by Apparently" footer. In it the merchant confirms their email with a one-time code, uploads the opinion, consents to share the result with us, and submits. We get the result in our Apparently Portfolio, and by signed webhook.

Read the codebase first. Find the onboarding step where a merchant should submit its legal opinion, the backend framework, and how secrets and env vars are configured. Follow the project's existing conventions. Do not add dependencies unless there is no reasonable alternative.

ENV VARS (server-side only; never ship them to a browser)
  APPARENTLY_API_KEY          ak_test_… while testing, ak_live_… in production (Portfolio → Settings → Developers)
  APPARENTLY_WEBHOOK_SECRET  whsec_… (shown once, when the webhook endpoint is registered)
  APPARENTLY_API_BASE         https://apparently.cc/api/v1/bank
Add them to the project's env example or config with placeholder values. Never commit real values.

ENDPOINTS (all under https://apparently.cc/api/v1/bank, with the header "Authorization: Bearer $APPARENTLY_API_KEY")
  POST /invites  (Idempotency-Key header required)      body { "email", "name", "external_ref" }  → data.id is the invitation id
  GET /invites/{id}
  POST /invites/{id}/apply-token  body { "return_url" }  → data { token: "bws_…", frame_url, hosted_url, expires_at }
  GET /applicants/{id}
  POST /webhooks  (Idempotency-Key header required)  body { "url", "events"? }  → the whsec_ secret, shown once
  POST /webhooks/{id}/test
  GET /events      the log webhooks are delivered from, to catch up on a missed delivery
  POST /sandbox/applicants/{id}/advance  sandbox keys only
Full reference: https://apparently.cc/banks/developers  ·  OpenAPI 3.1: https://apparently.cc/api/v1/bank/openapi.json

STEPS
1. Backend: when a merchant reaches the legal-opinion step, create the invitation once per merchant (reuse our own merchant id as external_ref, with a fresh Idempotency-Key per attempt that is stored and reused on retry), and store the returned invitation id on the merchant record.
2. Backend: add an authenticated endpoint on OUR server that calls POST /invites/{id}/apply-token for the current merchant and returns { token, frame_url } to our page. The API key stays on the server. An apply token lives 60 minutes; mint a fresh one per page load.
3. Frontend: on the onboarding page, load the script and mount the widget:
     <div id="apparently-apply"></div>
     <script src="https://apparently.cc/embed/apparently-bank.js"></script>
     const widget = ApparentlyBank.mount('#apparently-apply', {
       mode: 'apply', token, frameUrl,            // both from step 2
       returnUrl: '<the page after this step>',   // https; must be on a proved origin
       onEvent: (type, detail) => {},             // started, consented, document_uploaded, submitted, completed
       onSubmitted: () => {},                     // move the merchant to the next step
       onExpired: async () => widget.setToken(await fetchFreshApplyToken()),
     })
   If our onboarding cannot host an iframe, redirect the merchant to hosted_url instead.
4. Webhook: add a POST route (e.g. /webhooks/apparently) that reads the RAW body. Verify the "Apparently-Signature: t=<unix seconds>,v1=<hex>" header: v1 must equal HMAC-SHA256 of t + "." + raw body keyed with APPARENTLY_WEBHOOK_SECRET, compared in constant time, with t within 300 seconds of now. Answer 2xx fast, and deduplicate on the Apparently-Event-Id header. Record at least review.approved, review.conditions_set and review.denied on the merchant record. Events: invite.accepted, applicant.linked, review.started, review.findings_ready, review.revision_requested, review.conditions_set, review.approved, review.denied, monitoring.breach, document.requested, document.received.
5. Tests: unit-test the signature check (a valid signature, a tampered body, a stale timestamp) and the apply-token endpoint (it never returns the API key).

HUMAN STEPS (tell me which of these you could not do yourself)
- In Portfolio → Settings → Developers: mint a sandbox key, and list and prove the origin the onboarding page is served from (http://localhost is accepted for development). The widget can only be framed on a proved origin.
- In Dashboard → White label: set our name, logo, colour and support email.
- Register the webhook with POST /webhooks. It needs a public https URL, so use a tunnel in development, or poll GET /events.

TEST MODE
Use the ak_test_ key. Sandbox invitations are fixture applicants: nothing is emailed, nothing is charged, and the one-time code is always 000000. The outcome follows the invited email, like a test card: "+deny" is denied, "+conditions" is approved on conditions, anything else is approved. Move one along with POST /sandbox/applicants/{id}/advance, body { "to": "approved" }. Each move delivers its webhook at once.

DEFINITION OF DONE
- With the sandbox key, a test merchant reaches the step, the widget renders in our brand with "Powered by Apparently", the code 000000 verifies, a PDF uploads, consent is recorded, and submit fires onSubmitted.
- POST /sandbox/applicants/{id}/advance to approved delivers review.approved to our webhook, the signature verifies, and the merchant record shows approved.
- The API key and webhook secret appear only in server code and env config, never in client bundles, logs or the repo.
- The new tests pass. Summarise what you changed, which env vars to set, and the human steps still open.
```

## Every endpoint

Base URL: `https://apparently.cc/api/v1/bank`. Authentication: `Authorization: Bearer ak_live_…` (or `ak_test_…`).

| Route | Scope or auth | What it does |
|---|---|---|
| `GET /me` | key | The organisation, mode and scopes of the calling key |
| `POST /invites` | `invites:write` | Invite an applicant to a legal-opinion review (Idempotency-Key required) |
| `POST /invites/bulk` | `invites:write` | Invite up to 200 applicants at once; each row reports its own outcome (Idempotency-Key required) |
| `GET /invites/{id}` | `invites:write` | One invitation and where it stands |
| `POST /invites/{id}/widget-token` | `invites:write` | A short-lived token for the embeddable widget |
| `GET /applicants` | `reviews:read` | Every applicant: lane, status, SLA, checklist, current review |
| `GET /applicants/{id}` | `reviews:read` | One applicant |
| `GET /reviews/{id}` | `reviews:read` | One review: client-safe findings, revisions, decision, conditions, links |
| `GET /book` | `book:read` | The whole book, with risk and status |
| `GET /events` | `reviews:read` | The event log webhooks are delivered from, newest first |
| `GET /standards` | `reviews:read` | Your RLO review standard: every activated version, newest first, and the Apparently baseline |
| `GET /standards/active` | `reviews:read` | The version new reviews are examined under (your active one, or the Apparently baseline) |
| `POST /webhooks` | `webhooks:manage` | Register a webhook endpoint (the signing secret is shown once) (Idempotency-Key required) |
| `GET /webhooks` | `webhooks:manage` | Webhook endpoints |
| `GET /webhooks/{id}` | `webhooks:manage` | One endpoint and its recent deliveries |
| `DELETE /webhooks/{id}` | `webhooks:manage` | Stop delivering to an endpoint |
| `POST /webhooks/{id}/test` | `webhooks:manage` | Send a signed webhook.test event now |
| `POST /sandbox/applicants/{id}/advance` | `invites:write` | Sandbox only: move a fixture applicant to its next stage |
| `POST /sandbox/applicants/{id}/simulate` | `invites:write` | Sandbox only: raise a monitoring or document event |
| `GET /widget/session` | widget | What the widget shows (widget token, not an API key) |
| `POST /invites/{id}/apply-token` | `invites:write` | A 60-minute apply token for the white-labelled apply widget, and its hosted link |
| `GET /apply/session` | apply | What the apply widget shows: partner branding, the masked applicant email, the steps done |
| `POST /apply/code` | apply | Email a one-time code to the invited applicant |
| `POST /apply/verify` | apply | Exchange the one-time code for an apply session |
| `POST /apply/consent` | apply_session | Record the applicant's consent to share the result with the partner |
| `POST /apply/upload-url` | apply_session | A signed upload for the legal opinion or a supporting document |
| `POST /apply/uploads` | apply_session | Check an uploaded file (type, size, virus scan) and add it to the review |
| `POST /apply/website` | apply_session | Link the applicant's website and its public documents |
| `POST /apply/submit` | apply_session | Submit for review (consent required first) |
| `GET /openapi.json` | none | This API as an OpenAPI 3.1 document |

## Webhook events

- `invite.accepted`
- `applicant.linked`
- `review.started`
- `review.findings_ready`
- `review.revision_requested`
- `review.conditions_set`
- `review.approved`
- `review.denied`
- `monitoring.breach`
- `document.requested`
- `document.received`

Signature: `Apparently-Signature: t=<unix seconds>,v1=<hex>`, where v1 = HMAC-SHA256(secret, t + "." + raw body). Tolerance 300 seconds. Deduplicate on `Apparently-Event-Id`.
