Export Typeform to Qlik: CSV, Google Sheets, or the REST Connector
← All posts
GuideOct 31, 2026· 9 min read

Export Typeform to Qlik: CSV, Google Sheets, or the REST Connector

To export Typeform to Qlik, choose between three routes. For a one-off app, download your responses as CSV or XLSX and upload the file to Qlik Sense. For a feed that updates without code, send responses to Google Sheets with Typeform’s integration and load that sheet through Qlik’s Google Drive connector. For full control, create a Qlik REST connection to the Typeform Responses API and page through it in the load script, so every reload pulls the latest answers. Qlik Sense has no Typeform connector of its own on the analytics side, so each option goes through a file, a sheet, or the API. Qlik’s data integration products can replicate Typeform into a warehouse, which is a fourth option for larger setups.

This guide covers each route, with a working load script for the API. If you only need the raw data, the Export Typeform to JSON guide covers downloads and API output on their own. If your team reports in Power BI rather than Qlik, the Typeform to Power BI guide walks through the same three routes with Power Query.

Route 1: Upload a CSV or XLSX

This is the quickest way to get a sheet of charts in front of someone.

  1. In Typeform, open the form, go to Results, then Responses.
  2. Select the rows you want, click the export icon, and choose CSV or XLSX. Typeform’s export help page notes that large exports are sent by email rather than downloaded directly.
  3. In Qlik Sense, add the file as a data source (drag it into the app or upload it under data files), then load it through Data manager or the Data load editor.

Know the export’s quirks before you build master items on it:

  • A multi-select question becomes one column per answer choice, not a single column. A question with eight options gives you eight columns, which you will usually want to fold back into rows with CrossTable in the load script.
  • Yes/No answers come out as 1 and 0.
  • Start Date and Submit Date are in UTC, while Typeform’s interface shows local time.
  • Statements and question group titles are left out, because they hold no answers.

The file is a snapshot. Every refresh means someone downloads a new export and replaces the old one, so this route suits a single analysis better than a dashboard people check every week.

Route 2: Google Sheets in the middle

If you want new responses to appear on reload and nobody wants to touch the load script, put Google Sheets between the two tools.

On the Typeform side, open the form’s Connect panel and connect Google Sheets. Typeform’s setup guide explains that each submission becomes a row, with one column per question plus Submitted At and Token, the unique response ID.

On the Qlik side, the Google Drive connector in Qlik Cloud can browse Google Sheets files next to your other Drive files. Pick Google Sheets in the file type menu, select the spreadsheet and the tab for your form, and add it to the app. The connector preview stops at 50 rows, but the load itself is not limited. Schedule a reload on the app and the sheet’s new rows come in each time.

Limits worth planning for:

  • When you connect a form that already has responses, Typeform only backfills them for forms with up to 1,000 results, and partial responses are not backfilled. Older history needs a CSV upload on top.
  • Typeform’s guide says the integration is deactivated once the spreadsheet reaches 2 million cells, counting all tabs.
  • If you send partial responses to integrations, one respondent can produce two rows, one at the partial submit point and one when they finish. Deduplicate on Token in the load script.
  • The app is only as fresh as its last reload, so pick a reload schedule that matches how often people read it.

Route 3: The Responses API through a REST connection

This route skips the copy in the middle. Typeform stays the source of truth, and the load script gets every field the API returns, including hidden fields and calculated scores.

Before you start

  • Create a Typeform personal access token under Account, then Personal tokens, with read-only scopes for responses and forms. Typeform’s token documentation points out that anyone holding the token can act on your account’s data, so keep its scopes narrow.
  • Check which data center your account uses. Typeform’s EU data center page explains that asking the wrong domain for responses returns empty results. US accounts use api.typeform.com. EU accounts use api.eu.typeform.com, or api.typeform.eu if they were onboarded on the newer EU data center.
  • Note your form ID. It is the short string in the form’s URL, such as u6nXL7.

Create the connection

In the Data load editor, create a new connection and choose REST. Qlik’s REST connection guide lists every option; these are the ones that matter here:

  • URL: https://api.typeform.com/forms/u6nXL7/responses, with your own form ID and regional domain.
  • Method: GET, with authentication set to Anonymous.
  • Query headers: name Authorization, value Bearer tfp_your_token_here, with Encrypt ticked so the token is not stored in plain text.
  • Allow “WITH CONNECTION”: ticked. The script below overrides query parameters per request, and the connector throws an error if this option is off.
  • Pagination type: None. The script handles paging itself.

Name the connection Typeform_Responses.

Why the script pages by hand

Qlik’s REST connector has built-in pagination modes for offsets, page numbers, next tokens, and next URLs. Typeform’s Retrieve responses endpoint does not fit any of them cleanly. It returns up to 1,000 items per page, newest first, and you fetch the next page by passing the token of the last item you received as the before parameter. The response body has no next-page field to point the connector at. So the script loops: load a page, read the last token, ask for the page before it, and stop when a page comes back empty.

The load script

LIB CONNECT TO 'Typeform_Responses';

LET vBeforeClause = '';
LET vPage = 0;
LET vRows = 0;

DO
    LET vPage = $(vPage) + 1;

    RestConnectorMasterTable:
    SQL SELECT
        "total_items",
        "__KEY_root",
        (SELECT
            "response_id",
            "token",
            "landed_at",
            "submitted_at",
            "__KEY_items",
            "__FK_items",
            (SELECT
                "type" AS "answer_type",
                "text",
                "email",
                "number",
                "boolean",
                "date",
                "url",
                "phone_number",
                "__KEY_answers",
                "__FK_answers",
                (SELECT "ref", "__FK_field" FROM "field" FK "__FK_field"),
                (SELECT "label", "other", "__FK_choice" FROM "choice" FK "__FK_choice")
            FROM "answers" PK "__KEY_answers" FK "__FK_answers")
        FROM "items" PK "__KEY_items" FK "__FK_items")
    FROM JSON (wrap on) "root" PK "__KEY_root"
    WITH CONNECTION (QUERY "page_size" "1000" $(vBeforeClause));

    // Generated keys restart on every request, so resolve them per page.
    MapItem$(vPage):
    MAPPING LOAD [__KEY_items], [response_id]
    RESIDENT RestConnectorMasterTable WHERE NOT IsNull([__FK_items]);

    MapRef$(vPage):
    MAPPING LOAD [__FK_field], [ref]
    RESIDENT RestConnectorMasterTable WHERE NOT IsNull([__FK_field]);

    MapChoice$(vPage):
    MAPPING LOAD [__FK_choice], If(Len([label]) > 0, [label], [other])
    RESIDENT RestConnectorMasterTable WHERE NOT IsNull([__FK_choice]);

    Responses:
    LOAD
        [response_id],
        [token],
        Timestamp#(Replace(Left([landed_at], 19), 'T', ' '), 'YYYY-MM-DD hh:mm:ss') AS landed_at_utc,
        Timestamp#(Replace(Left([submitted_at], 19), 'T', ' '), 'YYYY-MM-DD hh:mm:ss') AS submitted_at_utc
    RESIDENT RestConnectorMasterTable WHERE NOT IsNull([__FK_items]);

    Answers:
    LOAD
        ApplyMap('MapItem$(vPage)', [__FK_answers]) AS response_id,
        ApplyMap('MapRef$(vPage)', [__KEY_answers]) AS question_ref,
        [answer_type],
        Coalesce([text], [email], [number], [boolean], [date], [url], [phone_number],
                 ApplyMap('MapChoice$(vPage)', [__KEY_answers], Null())) AS answer_value
    RESIDENT RestConnectorMasterTable WHERE NOT IsNull([__FK_answers]);

    DROP TABLE RestConnectorMasterTable;

    LET vNewRows = NoOfRows('Responses') - $(vRows);
    LET vRows = NoOfRows('Responses');
    EXIT DO WHEN $(vNewRows) = 0;

    LET vBeforeClause = ', QUERY "before" "' & Peek('token', -1, 'Responses') & '"';
LOOP

A few details in there are deliberate:

  • The first request has no before parameter. vBeforeClause starts empty, so the first call asks for the newest page. After each page, the script writes the last token into that variable and dollar-sign expansion drops it into the next WITH CONNECTION.
  • Keys are resolved inside the loop. The connector numbers __KEY_items and __KEY_answers from scratch on each request, so page two reuses the keys from page one. Mapping each page’s keys to the stable response_id and question ref before dropping the master table keeps answers attached to the right response.
  • Timestamps stay in UTC. Typeform returns ISO 8601 strings ending in Z. The script parses them as UTC and names the fields to match, so nobody mistakes them for local time.
  • Only completed responses come back unless you add response_type to the query. Typeform also notes that very recent submissions, roughly the last 30 minutes, may not appear yet. For a scheduled reload that does not matter.

The result is two tables linked on response_id: one row per response, and one row per answer with question_ref and answer_value. Keep the answers long for counts and distributions. Answer types the script does not list, such as multi-select choices and payments, come out with a null value; add another subselect for them if your form uses them. Hidden fields sit in a hidden object on each item, so select them in a further subselect under items if your form passes UTM tags or IDs.

Question titles

Responses carry the question’s ref but not its title. Load the form definition with the same connection by overriding the URL:

FormMaster:
SQL SELECT
    "__KEY_root",
    (SELECT "ref", "title", "__FK_fields" FROM "fields" FK "__FK_fields")
FROM JSON (wrap on) "root" PK "__KEY_root"
WITH CONNECTION (URL "https://api.typeform.com/forms/u6nXL7");

Questions:
LOAD [ref] AS question_ref, [title] AS question_title
RESIDENT FormMaster WHERE NOT IsNull([__FK_fields]);

DROP TABLE FormMaster;

Qlik associates Questions with Answers on question_ref automatically. Questions inside a question group are nested one level deeper in the form definition, so add a subselect for them if you use groups. Setting readable refs on each question in the Typeform builder before launch, such as company_size instead of a random string, makes the whole model easier to read.

Keeping reloads short

The script pulls the full history on every reload, which is fine for a few thousand responses. For bigger forms, store the result in a QVD and only fetch new responses: add QUERY "since" "..." with the latest submitted_at from the stored QVD, then concatenate the new rows onto the old ones. That keeps each reload to a single API page on most days.

Route 4: Qlik data integration into a warehouse

If Typeform is one source among many and the data should land in a warehouse first, Qlik’s data integration side lists Typeform as a SaaS source. It authenticates with the client ID and secret of a Typeform developer app, takes a start date and an optional list of form IDs, and replicates tables for forms, questions, answers, and submitted and unsubmitted landings. Your Qlik Sense app then loads from the target tables like any other database. This needs a Qlik subscription that includes data integration, so check your plan before you design around it. Outside Qlik, the Typeform to Redshift guide covers warehouse pipelines with other tools.

Responses that never reached Typeform

Some answers never reach the API: printed copies of the survey filled in at an event, PDFs a partner sends back, a phone photo of a sign-up sheet. If they belong in the same app, extract them into the same shape.

With ocrskill.com you list the fields you want and get JSON back:

curl "https://api.ocrskill.com/ocr.json?fields=last_name,first_name,email,company_name?" \
  -H "Authorization: Bearer sk-your-key-here" \
  -F "file=@event-survey-scan.jpg"

Required fields return a 400 when the page cannot support them, so a blurry scan fails loudly instead of becoming a half-empty row. Optional keys end in ?. Save the results as JSON or CSV files, load them in the same script with a source field set to 'paper', and use the file name as response_id so they sit alongside Typeform responses without colliding. The form data extraction API post covers form OCR in more depth, and the OCR JSON API reference lists the supported field names.

Which route to pick

Situation Route
One analysis, one time CSV or XLSX upload
Live feed, no scripting, within the sheet limits Typeform to Google Sheets to Qlik
Scheduled reloads, hidden fields, full history REST connection to the Responses API
Typeform plus many other sources in a warehouse Qlik data integration or another ELT tool
Paper, PDF, or photo forms OCR to JSON, then load alongside

Conclusion

Qlik and Typeform do not connect directly on the analytics side, but none of the routes is hard. A CSV gets you a chart today, Google Sheets gives you a live feed with no script, and a REST connection gives you every field the API exposes on a reload schedule. If you take the API route, the parts that decide whether it keeps working are small: query the right regional domain, keep the token in an encrypted header, loop on the before token because the connector’s built-in paging cannot follow Typeform’s cursor, and resolve the connector’s generated keys on each page so answers stay attached to the right response.