Logo

How to Make a PDF Form Fillable Online and Collect the Submissions

You have a PDF form — a vendor intake, a client questionnaire, a field report. Today it goes out as an email attachment, and what comes back is a mess: printed, scanned, half-filled copies whose answers someone re-types into a spreadsheet.

The usual fix is rebuilding the whole document in a form-builder tool. But your PDF already has the fields. In this tutorial you'll keep it exactly as it is: share it as a link that's fillable in the browser, and collect every submission as typed, recipient-attributed data.

What you'll build
Your PDF form, fillable at a link — and every submission arriving as structured JSON: who submitted, and every field with its type and value. Collected in your dashboard, pulled over the API, or pushed to your webhook the moment it happens: Dana Kim submitted “Vendor Intake Form” — VendorCo GmbH, Growth tier
About 15 minutes Free plan for forms & submissions · the webhook step needs Pro (14-day trial, no card)
1

Put your fillable PDF behind a tracking link

Upload your PDF in the Apdf dashboard or via the API. Any standard PDF form fields work — text inputs, radio groups, checkboxes, multi-line text. The hosted viewer renders them fillable in any browser, no install for your recipients.

curl -X POST https://apdf.io/api/docs \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json" \
  -d file="https://your-server.com/vendor-intake-form.pdf" \
  -d name="Vendor Intake Form"
{
    "data": {
        "doc_id": "ebded-b6c48-000c5",
        "viewer_url": "https://docs.apdf.io/studio/ebded-b6c48-000c5",
        "file_url": "https://apdf-files.s3.eu-central-1.amazonaws.com/docs/202607/3911b2126a607fe1f287d.pdf",
        "name": "Vendor Intake Form",
        "file_size": 8159,
        "file_pages": 1,
        "is_public": false,
        "created_at": "2026-07-22T08:45:12.000000Z",
        "archived_at": null
    }
}
Tip: First document? Your workspace needs a handle first — it becomes part of your viewer URLs (docs.apdf.io/your-handle/…). The dashboard asks for one before your first upload; API users can set it in the account settings.
2

Send each recipient their own link

Mint a recipient link per person instead of sharing the document URL. Each recipient gets a tokenized URL, so every submission comes back already attributed — no “who sent this scan?” detective work.

curl -X POST https://apdf.io/api/docs/ebded-b6c48-000c5/links \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json" \
  -d name="Dana Kim" \
  -d email="dana@vendorco.example"
{
    "data": {
        "token": "GhMG25wO",
        "name": "Dana Kim",
        "email": "dana@vendorco.example",
        "url": "https://docs.apdf.io/studio/ebded-b6c48-000c5?v=GhMG25wO",
        "is_active": true,
        "last_viewed_at": null,
        "created_at": "2026-07-22T08:45:48.000000Z"
    }
}

Send that url to Dana. She opens it in her browser, fills the fields right in the PDF, and hits Submit — that's her entire experience. No account, no app, no printout.

3

Watch the submissions arrive

Every submission lands in your dashboard, typed and attributed. Open your document under Documents and switch to the Form Submits tab:

The Form Submits tab showing three submissions from different recipients, with one expanded into its typed fields: company name, contact email, a radio choice, a checkbox and a notes field

The same data is available over the API — each submission carries the recipient and every field with its type, page and value:

curl https://apdf.io/api/docs/ebded-b6c48-000c5/form-submissions \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"
{
    "data": [
        {
            "doc_id": "ebded-b6c48-000c5",
            "creator_id": "Z18UJTfOI87B",
            "recipient": {
                "name": "Dana Kim",
                "email": "dana@vendorco.example"
            },
            "fields": [
                { "id": "10R", "name": "company_name", "page": 1, "type": "text", "value": "VendorCo GmbH" },
                { "id": "14R", "name": "contact_email", "page": 1, "type": "text", "value": "dana@vendorco.example" },
                { "id": "17R", "name": "service_tier", "page": 1, "type": "radio", "value": false, "checked": false, "exportValue": "Starter" },
                { "id": "22R", "name": "service_tier", "page": 1, "type": "radio", "value": true, "checked": true, "exportValue": "Growth" },
                { "id": "27R", "name": "service_tier", "page": 1, "type": "radio", "value": false, "checked": false, "exportValue": "Enterprise" },
                { "id": "33R", "name": "nda_accepted", "page": 1, "type": "checkbox", "value": true, "checked": true, "exportValue": "Yes" },
                { "id": "39R", "name": "notes", "page": 1, "type": "textarea", "value": "We can start onboarding next week." }
            ],
            "created_at": "2026-07-22T08:46:02.000000Z",
            "updated_at": "2026-07-22T08:46:02.000000Z"
        }
    ]
}

Note how choice fields work: every radio option is reported with its exportValue, and the picked one is checked. No parsing, no flattened text blob.

4

Push every submission to your webhook

Checking a dashboard is fine; not having to check it is better. Wire a when→then automation so each submission POSTs itself to your endpoint the moment it happens:

  1. Go to Automations in the dashboard and click New Automation.
  2. Under “When this happens…” pick Form Submitted as the trigger. Scope it to all documents or just this one.
  3. Under “Then do this…” paste your Webhook URL and save.
Plan note: Automations are a Pro feature. Every new account can trial Pro for 14 days — no credit card — which covers everything in this tutorial.

From now on, each submission arrives at your endpoint looking like this:

{
    "automation": {
        "id": "57a30-8b123-ab852",
        "name": "Forward form submissions"
    },
    "document": {
        "doc_id": "ebded-b6c48-000c5",
        "name": "Vendor Intake Form",
        "viewer_url": "https://docs.apdf.io/studio/ebded-b6c48-000c5",
        "file_pages": 1
    },
    "trigger": {
        "matched_event": "form:submit",
        "conditions_evaluated": false
    },
    "event": {
        "session_id": "ky4NcNa69ILv06To",
        "viewer_id": "Z18UJTfOI87B",
        "recipient": {
            "name": "Dana Kim",
            "email": "dana@vendorco.example"
        },
        "data": {
            "fields": [
                { "id": "10R", "name": "company_name", "type": "text", "value": "VendorCo GmbH", "page": 1 },
                { "id": "14R", "name": "contact_email", "type": "text", "value": "dana@vendorco.example", "page": 1 },
                { "id": "17R", "name": "service_tier", "type": "radio", "value": false, "page": 1, "exportValue": "Starter", "checked": false },
                { "id": "22R", "name": "service_tier", "type": "radio", "value": true, "page": 1, "exportValue": "Growth", "checked": true },
                { "id": "27R", "name": "service_tier", "type": "radio", "value": false, "page": 1, "exportValue": "Enterprise", "checked": false },
                { "id": "33R", "name": "nda_accepted", "type": "checkbox", "value": true, "page": 1, "exportValue": "Yes", "checked": true },
                { "id": "39R", "name": "notes", "type": "textarea", "value": "We can start onboarding next week.", "page": 1 }
            ]
        }
    },
    "session": {
        "total_duration_ms": 4900,
        "pages_viewed": 1,
        "completion_rate": 100
    },
    "timestamp": "2026-07-22T08:46:02+00:00"
}
5

Handle the submission in PHP

A complete receiver in one file — it collapses the fields into a clean name => value array (keeping only the checked option of each radio and checkbox) and hands the result to whatever comes next in your stack:

<?php

// Receive a form submission webhook and hand it to your CRM
$payload = json_decode(file_get_contents('php://input'), true);

$recipient = $payload['event']['recipient'] ?? null;
$values = collect_fields($payload['event']['data']['fields']);

// Every submitted value, keyed by field name — checked radios/checkboxes only
function collect_fields(array $fields): array
{
    $values = [];

    foreach ($fields as $field) {
        if (in_array($field['type'], ['radio', 'checkbox'], true)) {
            if ($field['checked']) {
                $values[$field['name']] = $field['exportValue'];
            }
            continue;
        }

        $values[$field['name']] = $field['value'];
    }

    return $values;
}

// From here it's your world: create the CRM record, notify the team, ...
error_log(sprintf(
    'New vendor intake from %s <%s>: %s (%s tier)',
    $recipient['name'] ?? 'Unknown',
    $recipient['email'] ?? '-',
    $values['company_name'] ?? '?',
    $values['service_tier'] ?? '?',
));

http_response_code(200);
echo json_encode(['received' => true]);
php -S 0.0.0.0:8080 webhook.php

Submit the form once more and your log shows the whole story in one line:

New vendor intake from Dana Kim <dana@vendorco.example>: VendorCo GmbH (Growth tier)

Where to go from here

The submission is one signal — the same document streams every reader event.

Ready to see who reads?