> ## Documentation Index
> Fetch the complete documentation index at: https://docs.venture.caplia.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Venture Form Widget

> Drop a pitch-deck upload widget into any Webflow, WordPress, or HTML form. The PDF goes straight to Caplia for scoring; everything else in your form keeps flowing to your existing CRM. Built for VCs, accelerators, awards programmes, and corp-dev intakes.

**The Caplia Venture Form Widget is a single block of HTML you paste into your existing application form.** Entrants pick a PDF, the widget uploads it directly to Caplia, and writes the resulting job ID into a hidden form field that flows on to your CRM (HubSpot, Salesforce, anywhere). No backend changes on your side, no API key in the browser.

Built for any public form where the entrant uploads a pitch deck.

## Who it's for

<CardGroup cols={2}>
  <Card title="Venture funds" icon="building-columns">
    Your fund's public "submit your deck" form. Cold inbound lands in Caplia pre-scored, sorted by CRI quality and thesis fit, ready for partner review.
  </Card>

  <Card title="Accelerators" icon="rocket">
    Cohort application forms. Every submitted deck is scored on arrival; selection committees see a ranked shortlist instead of a Google Drive folder.
  </Card>

  <Card title="Awards programmes" icon="trophy">
    Innovation award entries, pitch competitions, founder prizes. Judges work from CRI scores and per-category thesis fit instead of reading every deck cold.
  </Card>

  <Card title="Corporate development" icon="handshake">
    Partnership intake forms. Inbound startup approaches are scored against your strategic thesis automatically before a BD lead picks them up.
  </Card>
</CardGroup>

## How it works

<Steps>
  <Step title="Entrant picks a PDF">
    The widget renders a styled "Choose pitch deck" button inside your existing form. It enforces PDF-only, 50 MB max, client-side and server-side.
  </Step>

  <Step title="Widget mints a one-shot upload token">
    A `POST /v1/uploads/sign` call returns a `cap_upload_<jwt>` token — 5-minute TTL, single-use, signed server-side. No long-lived API key in the browser.
  </Step>

  <Step title="Widget uploads the PDF to Caplia">
    The PDF posts to `POST /v1/uploads/decks` with the token. Caplia queues the deck for scoring (CRI quality score + thesis-fit) and returns a `job_id`.
  </Step>

  <Step title="The job ID flows on to your CRM">
    The widget writes the `job_id` into a hidden input named `caplia_job_id`. When the entrant submits the rest of the form, that ID lands in HubSpot (or wherever) alongside their other fields.
  </Step>

  <Step title="Scoring lands in your Caplia dashboard">
    The deck is processed asynchronously. Once scored, the entry appears in your Caplia pipeline with CRI score, per-thesis fit, and is queryable by the application ID you supplied.
  </Step>
</Steps>

## Onboarding

Before pasting the widget, get a **`form_id`** from your Caplia contact (Connor, or <a href="mailto:paul@caplia.ai">[paul@caplia.ai](mailto:paul@caplia.ai)</a>). It's an opaque string like `acme-2026` or `fund-x-inbound` that maps to your team and the user submissions land under. We allowlist it server-side in the same step.

## The snippet

Paste this whole block into a single **Webflow Embed element** (or any HTML container that allows inline `<script>`) placed inside your application form, where your "deck URL" field used to be.

```html theme={null}
<!-- Caplia Venture Form Widget v1 -->
<div data-caplia-upload
     data-form-id="REPLACE_WITH_FORM_ID"
     data-application-id-field="application_number"
     data-api-base="https://api.venture.caplia.ai">
  <input type="file" accept=".pdf,application/pdf" hidden>
  <button type="button" class="caplia-pick">Choose pitch deck (PDF, up to 50 MB)</button>
  <div class="caplia-status" role="status" aria-live="polite"></div>
  <input type="hidden" name="caplia_job_id" value="">
  <input type="hidden" name="caplia_deck_filename" value="">
</div>

<style>
  [data-caplia-upload] { font-family: inherit; color: inherit; }
  [data-caplia-upload] .caplia-pick {
    display: block; width: 100%; padding: 0.875rem 1rem;
    border: 1px dashed currentColor; border-radius: 0.375rem;
    background: transparent; color: inherit; font: inherit;
    cursor: pointer; text-align: center;
  }
  [data-caplia-upload] .caplia-pick:hover { background: rgba(0,0,0,0.04); }
  [data-caplia-upload].is-uploading .caplia-pick { opacity: 0.6; pointer-events: none; }
  [data-caplia-upload] .caplia-status { margin-top: 0.5rem; font-size: 0.875rem; min-height: 1.25rem; }
  [data-caplia-upload].is-error   .caplia-status { color: #b91c1c; }
  [data-caplia-upload].is-success .caplia-status { color: #15803d; }
</style>

<script>
(function () {
  document.querySelectorAll('[data-caplia-upload]:not([data-caplia-init])').forEach(function (root) {
    root.setAttribute('data-caplia-init','true');
    var formId=root.dataset.formId, appIdField=root.dataset.applicationIdField||'',
        apiBase=(root.dataset.apiBase||'https://api.venture.caplia.ai').replace(/\/+$/, '');
    var fileInput=root.querySelector('input[type="file"]'),
        pickBtn=root.querySelector('.caplia-pick'),
        statusEl=root.querySelector('.caplia-status'),
        jobIdInput=root.querySelector('input[name="caplia_job_id"]'),
        filenameInput=root.querySelector('input[name="caplia_deck_filename"]'),
        form=root.closest('form');

    if (!formId || formId==='REPLACE_WITH_FORM_ID') {
      statusEl.textContent='Widget not configured: missing form id.';
      root.classList.add('is-error'); return;
    }
    function setStatus(m,k){root.classList.remove('is-uploading','is-error','is-success');if(k)root.classList.add('is-'+k);statusEl.textContent=m||'';}
    function getApplicationId(){if(!appIdField||!form)return;var el=form.querySelector('[name="'+appIdField+'"]');var v=el&&(el.value||'').trim();return v||undefined;}
    pickBtn.addEventListener('click',function(){fileInput.click();});

    fileInput.addEventListener('change', async function(){
      var file=fileInput.files&&fileInput.files[0]; if(!file)return;
      if(file.type&&file.type!=='application/pdf'){setStatus('Please choose a PDF file.','error');return;}
      if(file.size===0){setStatus('That file appears to be empty.','error');return;}
      if(file.size>50*1024*1024){setStatus('File is larger than 50 MB.','error');return;}
      setStatus('Uploading '+file.name+'…','uploading'); jobIdInput.value=''; filenameInput.value='';
      try{
        var signRes=await fetch(apiBase+'/v1/uploads/sign',{method:'POST',headers:{'Content-Type':'application/json'},
          body:JSON.stringify({form_id:formId,application_id:getApplicationId()})});
        if(!signRes.ok){var e=await signRes.json().catch(function(){return{};});throw new Error((e.error&&e.error.message)||'Could not start upload ('+signRes.status+').');}
        var sign=await signRes.json();
        var fd=new FormData(); fd.append('file',file);
        var up=await fetch(apiBase+'/v1/uploads/decks',{method:'POST',headers:{'Authorization':'Bearer '+sign.token},body:fd});
        if(!up.ok){var e2=await up.json().catch(function(){return{};});throw new Error((e2.error&&e2.error.message)||'Upload failed ('+up.status+').');}
        var data=await up.json();
        jobIdInput.value=data.job_id; filenameInput.value=file.name;
        pickBtn.textContent='Replace pitch deck'; setStatus('Uploaded '+file.name+' ✓','success');
      }catch(e){console.error('caplia upload error',e);setStatus(e.message||'Upload failed. Please try again.','error');}
    });
    if(form){form.addEventListener('submit',function(e){if(root.classList.contains('is-uploading')){e.preventDefault();setStatus('Please wait for the pitch deck upload to finish.','error');}});}
  });
})();
</script>
```

## Configuration

Three data attributes on the root `<div>`:

| Attribute                   | Required | Description                                                                                                                                                                                                                                  |
| --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-form-id`              | **Yes**  | Opaque identifier issued by Caplia onboarding (e.g. `acme-2026`, `fund-x-inbound`). Must match the entry we allowlisted server-side, or the widget will refuse to upload.                                                                    |
| `data-application-id-field` | No       | The `name` attribute of an existing form field that holds the entrant's application number / candidate ID. The widget reads its value at upload time so the Caplia entry can be matched back to your CRM row. Omit it if you don't have one. |
| `data-api-base`             | No       | API origin override. Defaults to `https://api.venture.caplia.ai` (production). Set to `https://api-sandbox.venture.caplia.ai` during testing.                                                                                                |

## Fields the widget writes

Make sure your form has two corresponding fields (they can be hidden — the widget creates them if missing in pure HTML, but in Webflow you'll want to declare them so HubSpot can map them):

| Field name             | Purpose                                                                                                                                                                                              |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `caplia_job_id`        | The Caplia job ID (UUID). Map this to a CRM property like "Caplia Job ID". Use it later to fetch scoring data via [`GET /v1/jobs/{id}`](/api-reference/introduction) with your `cap_inv_live_*` key. |
| `caplia_deck_filename` | The entrant's original PDF filename. Useful for your CRM record.                                                                                                                                     |

## Webflow setup

<Steps>
  <Step title="Add an Embed element to your form">
    In the Webflow designer, drop a Code Embed component inside the Form block, where your old "deck URL" field used to be.
  </Step>

  <Step title="Paste the widget">
    Paste the snippet above into the Embed. Replace `REPLACE_WITH_FORM_ID` with the `form_id` Caplia gave you.
  </Step>

  <Step title="Set the application-id field name">
    Change `data-application-id-field="application_number"` to whatever the `name` of your existing Application Number / Candidate ID field is. (Right-click the field in Webflow → Settings → Name.)
  </Step>

  <Step title="Add hidden form fields for the widget's outputs">
    Add two **Form** components inside the same form, type "Text", with names `caplia_job_id` and `caplia_deck_filename`. Set them to hidden in the Webflow designer (or via custom CSS). HubSpot will receive their values on form submit.
  </Step>

  <Step title="Map the new fields in HubSpot">
    In your existing Webflow → HubSpot mapping, add the two new fields. `caplia_job_id` is the important one — store it as a contact (or deal) property so you can cross-reference back to Caplia later.
  </Step>

  <Step title="Test on the staging Webflow domain">
    Set `data-api-base="https://api-sandbox.venture.caplia.ai"` and use the sandbox `form_id` we provision for you. Submit a test entry, confirm the `caplia_job_id` lands in HubSpot, then flip to production by removing the `data-api-base` override.
  </Step>
</Steps>

## Endpoints used

The widget calls two endpoints. Both are public — no API key required from the browser.

### `POST /v1/uploads/sign`

Issues a short-lived `cap_upload_<jwt>` token bound to one upload. The `form_id` must be allowlisted server-side.

```bash theme={null}
curl -X POST https://api.venture.caplia.ai/v1/uploads/sign \
  -H "Content-Type: application/json" \
  -d '{"form_id":"acme-2026","application_id":"APP-1234"}'
```

```json theme={null}
{
  "token": "cap_upload_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_at": "2026-05-12T15:35:00.000Z",
  "max_bytes": 52428800,
  "accepted_mime": "application/pdf",
  "properties": [
    {
      "id": "a99031b5-…",
      "name": "Award Category",
      "options": [
        { "id": "5e2c…", "label": "Climate Tech" },
        { "id": "9b41…", "label": "Deep Tech" }
      ]
    }
  ]
}
```

The `properties` array is the form team's **live custom-property config** — render it as dropdowns (an awards programme's category picker, a fund's source tag) and the form stays current when the team renames or adds options inside Caplia. Pass selections on the upload call below.

### `POST /v1/uploads/decks`

Accepts the PDF using the upload token.

```bash theme={null}
curl -X POST https://api.venture.caplia.ai/v1/uploads/decks \
  -H "Authorization: Bearer cap_upload_eyJ..." \
  -F "file=@deck.pdf" \
  -F "company_name=Acme Inc" \
  -F "founder_name=Ada Lovelace" \
  -F "founder_email=ada@acme.com" \
  -F 'properties={"a99031b5-…":"5e2c…"}'
```

```json theme={null}
{
  "job_id": "a4f6c0d8-...",
  "company_id": null,
  "status": "queued",
  "poll_url": "https://api.venture.caplia.ai/v1/jobs/a4f6c0d8-..."
}
```

## Pulling scoring data back into your CRM

Once you have the `job_id` on the CRM side, your back-office tooling can fetch results with the regular Caplia REST API using a `cap_inv_live_*` key — see [Authentication](/authentication).

```bash theme={null}
# Poll the job (returns company_id once intake is done)
curl https://api.venture.caplia.ai/v1/jobs/a4f6c0d8-... \
  -H "Authorization: Bearer cap_inv_live_..."

# Then fetch CRI + thesis-fit scores
curl https://api.venture.caplia.ai/v1/companies/{company_id}/scores \
  -H "Authorization: Bearer cap_inv_live_..."
```

A scheduled HubSpot workflow (or Salesforce flow) that pings `/v1/jobs/{job_id}` every few minutes and writes scoring data back onto the contact is the simplest production pattern.

## Security model

<AccordionGroup>
  <Accordion title="Why isn't there an API key in the widget?" icon="lock">
    Long-lived `cap_inv_live_*` keys must never ship to a browser — anyone could view-source the page and use the key to push decks into your pipeline, or read your data. The two-step token flow (`/v1/uploads/sign` → `cap_upload_<jwt>`) is the same pattern AWS uses for S3 presigned POSTs: server-side authorisation, short-lived, single-use, scoped to one operation.
  </Accordion>

  <Accordion title="What stops random sites from spamming uploads?" icon="shield">
    Two gates. First, only `form_id`s that have been **explicitly allowlisted on the Caplia side** can mint tokens — random `form_id` strings return 404. Second, each token has a 5-minute TTL and is bound to a single upload. We add per-IP rate limits and optional Cloudflare Turnstile challenges on top once a programme grows beyond a small expected entrant pool.
  </Accordion>

  <Accordion title="Where does the deck land?" icon="folder">
    Encrypted Caplia storage, scoped to your team. Only members of your Caplia team can access entries. The deck never touches Webflow's or HubSpot's storage.
  </Accordion>

  <Accordion title="GDPR / data subject rights" icon="user-shield">
    Entrants' decks are processed on the basis of the consent they give in your application form. We provide tooling to bulk-export or bulk-delete a form's entries when a programme concludes — talk to Connor about retention windows.
  </Accordion>
</AccordionGroup>

## Limitations

* **PDF only.** Other file types are rejected by both the widget (client-side) and the API (server-side).
* **50 MB maximum.** Server-enforced; tokens carry the limit in their signed claims.
* **No upload progress bar yet.** The widget shows "Uploading…" but not byte-level progress; `fetch()` doesn't expose that without falling back to `XMLHttpRequest`. We'll add a progress bar in v2 if customer feedback warrants it.
* **One widget instance per form.** Multiple instances on the same page are supported (each is initialised independently), but two instances inside the same `<form>` would clobber each other's hidden inputs.

## Need help?

* **Onboarding & `form_id` provisioning**: <a href="mailto:paul@caplia.ai">[paul@caplia.ai](mailto:paul@caplia.ai)</a>
* **REST API docs**: [API Reference](/api-reference/introduction)
* **Authentication**: [Authentication](/authentication)
