Skip to main content

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.

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

Venture funds

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.

Accelerators

Cohort application forms. Every submitted deck is scored on arrival; selection committees see a ranked shortlist instead of a Google Drive folder.

Awards programmes

Innovation award entries, pitch competitions, founder prizes. Judges work from CRI scores and per-category thesis fit instead of reading every deck cold.

Corporate development

Partnership intake forms. Inbound startup approaches are scored against your strategic thesis automatically before a BD lead picks them up.

How it works

1

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.
2

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.
3

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.
4

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.
5

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.

Onboarding

Before pasting the widget, get a form_id from your Caplia contact (Connor, or paul@caplia.ai). 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.
<!-- 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>:
AttributeRequiredDescription
data-form-idYesOpaque 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-fieldNoThe 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-baseNoAPI 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 namePurpose
caplia_job_idThe 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} with your cap_inv_live_* key.
caplia_deck_filenameThe entrant’s original PDF filename. Useful for your CRM record.

Webflow setup

1

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.
2

Paste the widget

Paste the snippet above into the Embed. Replace REPLACE_WITH_FORM_ID with the form_id Caplia gave you.
3

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.)
4

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.
5

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.
6

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.

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.
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"}'
{
  "token": "cap_upload_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_at": "2026-05-12T15:35:00.000Z",
  "max_bytes": 52428800,
  "accepted_mime": "application/pdf"
}

POST /v1/uploads/decks

Accepts the PDF using the upload token.
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"
{
  "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.
# 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

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/signcap_upload_<jwt>) is the same pattern AWS uses for S3 presigned POSTs: server-side authorisation, short-lived, single-use, scoped to one operation.
Two gates. First, only form_ids 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.
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.
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.

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?