Export Typeform to Redshift: AppFlow, ELT Connectors, or a COPY Pipeline
To export Typeform to Redshift, you have three realistic routes. Amazon AppFlow has a Typeform source and a Redshift destination, so you can schedule a flow without leaving AWS. Managed ELT tools such as Fivetran and Airbyte sync Typeform into Redshift tables for you. Or you write a short job that pulls the Responses API, drops newline-delimited JSON in S3, and loads it with COPY into a SUPER column. Typeform itself does not ship a Redshift integration; its community team points people to Zapier instead, which works for low volume but is a row-at-a-time tool, not a warehouse loader.
This guide covers each route, the schema you end up with, and how to bring paper or scanned forms into the same tables. If you only need the raw files, the Export Typeform to JSON guide covers the Responses API and CSV downloads on their own.
Route 1: Amazon AppFlow
AppFlow is the shortest path if your team already lives in AWS. The Typeform connector page lists Amazon Redshift among the supported destinations, alongside S3, RDS for PostgreSQL, and Snowflake.
The setup has two halves:
- Typeform credentials. Create either a personal access token with the scopes for the objects you need, or a developer app for OAuth 2.0 with a redirect URL in the form
https://<region>.console.aws.amazon.com/appflow/oauth. - A Redshift connection in AppFlow. AWS recommends the Redshift Data API for the connection, and the setup also needs an intermediate S3 bucket and IAM roles so AppFlow can stage data before loading it.
Then create a flow, choose Typeform as the source and Response as the object, and map fields to your table. The Response object exposes response_id, submitted_at, landed_at, completed, hidden, metadata, calculated, and answers. It also supports since and until filters, which is how you keep a scheduled flow incremental.
Two things to plan for. answers arrives as a list, one entry per question, so you will still want a view that turns it into rows or columns. And the Form object is separate, so join on form metadata yourself if you need question titles next to answers.
Route 2: Fivetran, Airbyte, or another ELT tool
If you already pay for an ELT platform, this is the low-maintenance option.
- Fivetran has a Typeform connector that loads normalized tables such as
RESPONSE,RESPONSE_ANSWER, andRESPONSE_ANSWER_CHOICE, plus form history tables, into a Redshift destination. Responses sync incrementally. Its docs are explicit that deleted responses cannot be captured, because the Typeform API does not report them. - Airbyte has a Typeform source with an incremental Responses stream keyed on
response_id. Without a start date it fetches one year of responses, so set one if you need older history. Its docs also note that the connector uses the US API endpoint, which matters if your account stores responses in Typeform’s EU data center.
The trade-off is the usual one: you get pre-built tables and retries, and you accept the vendor’s schema, sync schedule, and pricing. Check how each tool counts rows before you point it at a high-volume form.
Route 3: Responses API to S3 to COPY
When you want full control over the schema, or you only need one or two forms, the do-it-yourself pipeline is small. The idea is to store each response as one JSON document in a SUPER column, then query or flatten it with SQL.
Step 1: pull responses as newline-delimited JSON
The Retrieve responses endpoint returns up to 1,000 items per page. Responses come back newest first, so passing the last item’s token as before walks back through history, and since limits a scheduled run to recent submissions.
import json
import os
import boto3
import requests
TOKEN = os.environ["TYPEFORM_TOKEN"]
FORM_ID = "u6nXL7"
BUCKET = "my-warehouse-landing"
KEY = f"typeform/{FORM_ID}/responses.jsonl"
def fetch_responses(since=None):
params = {"page_size": 1000, "completed": "true"}
if since:
params["since"] = since
while True:
r = requests.get(
f"https://api.typeform.com/forms/{FORM_ID}/responses",
headers={"Authorization": f"Bearer {TOKEN}"},
params=params,
timeout=60,
)
r.raise_for_status()
items = r.json().get("items", [])
if not items:
return
yield from items
params["before"] = items[-1]["token"]
lines = []
for item in fetch_responses(since="2026-01-01T00:00:00Z"):
item["form_id"] = FORM_ID
lines.append(json.dumps(item))
boto3.client("s3").put_object(
Bucket=BUCKET, Key=KEY, Body="\n".join(lines).encode("utf-8")
)
Write one object per line rather than one big JSON array. Each line becomes one row on load, and Redshift caps a single JSON object at 4 MB, which a whole export could exceed but a single response will not.
Step 2: load into a SUPER column
Redshift’s COPY into SUPER guide describes FORMAT JSON 'noshred', which stores each document whole instead of matching keys to columns. That is what you want for form data, because the set of questions changes every time someone edits the form.
CREATE TABLE typeform_responses (
response_id VARCHAR(64) NOT NULL,
form_id VARCHAR(32) NOT NULL,
submitted_at TIMESTAMPTZ,
payload SUPER
);
CREATE TEMP TABLE typeform_stage (doc SUPER);
COPY typeform_stage
FROM 's3://my-warehouse-landing/typeform/u6nXL7/responses.jsonl'
IAM_ROLE 'arn:aws:iam::123456789012:role/RedshiftLoad'
FORMAT JSON 'noshred';
Step 3: merge so reruns stay safe
A scheduled job will see the same response twice sooner or later. MERGE keyed on response_id keeps the table free of duplicates:
MERGE INTO typeform_responses
USING (
SELECT doc.response_id::VARCHAR AS response_id,
doc.form_id::VARCHAR AS form_id,
doc.submitted_at::VARCHAR::TIMESTAMPTZ AS submitted_at,
doc AS payload
FROM typeform_stage
) s
ON typeform_responses.response_id = s.response_id
WHEN MATCHED THEN UPDATE SET
submitted_at = s.submitted_at,
payload = s.payload
WHEN NOT MATCHED THEN INSERT
VALUES (s.response_id, s.form_id, s.submitted_at, s.payload);
Step 4: flatten answers with PartiQL
Each answer carries the question’s field.ref, a type, and a value stored under a key named after that type (text, email, number, choice, and so on). Redshift’s PartiQL navigation lets you unnest the array in the FROM clause:
CREATE VIEW typeform_answers AS
SELECT r.response_id,
r.form_id,
r.submitted_at,
a.field.ref::VARCHAR AS question_ref,
a.type::VARCHAR AS answer_type,
CASE a.type::VARCHAR
WHEN 'text' THEN a.text::VARCHAR
WHEN 'email' THEN a.email::VARCHAR
WHEN 'date' THEN a.date::VARCHAR
WHEN 'url' THEN a.url::VARCHAR
WHEN 'phone_number' THEN a.phone_number::VARCHAR
WHEN 'choice' THEN a.choice.label::VARCHAR
ELSE JSON_SERIALIZE(a)
END AS answer_value
FROM typeform_responses r, r.payload.answers a;
Numbers, booleans, and multi-select choices fall through to JSON_SERIALIZE, so nothing is silently dropped. Add explicit branches once you know which types your form uses. From here a BI tool can pivot question_ref into columns, or you can build a per-form view with one column per ref.
A few details that save time:
- Set question refs in Typeform. Auto-generated refs are long UUID-like strings. Giving each question a readable ref before launch makes every downstream query easier to read.
- Watch attribute case. AWS recommends turning on
enable_case_sensitive_super_attributewhen working withSUPER, which matters if any of your refs or hidden field names use capitals. - Decide on partials. The script above asks for
completed=true. Drop that filter if you also want partial submissions, and store thecompletedflag so reports can tell them apart.
When the form never reached Typeform
Some responses will never have a response_id: a printed copy of the survey filled in at an event, a PDF a partner emailed back, a photo of a signup sheet. If your reporting should include them, read the page into JSON with the same keys and load it next to the Typeform data.
With OCRskill 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-signup-scan.jpg"
Required fields return a 400 when the page cannot support them, so an unreadable scan does not become a half-empty row. Optional keys end in ?. Write each result as one JSON line with a source tag and the file name as its ID, load it with the same COPY ... FORMAT JSON 'noshred', and expose both tables through one view with a source column. The form data extraction API post covers form OCR in more depth, and the OCR JSON API reference lists supported field names. If the same answers also feed a project board, the Typeform to monday.com guide uses the identical field mapping idea.
Which route to pick
| Situation | Route |
|---|---|
| AWS-only stack, scheduled loads, little code | Amazon AppFlow |
| Many SaaS sources already in an ELT tool | Fivetran or Airbyte |
| One or two forms, custom schema, cheap reruns | Responses API, S3, COPY into SUPER, MERGE |
| Paper, PDF, or photo forms | OCR to JSON, then the same COPY path |
Conclusion
There is no button in Typeform that sends responses to Redshift, but every route above ends in the same place: one row per response keyed on response_id, with answers you can unnest by question ref. AppFlow and the ELT tools get you there with configuration, and the hand-built pipeline gets you there with about forty lines of Python and three SQL statements. Whichever you choose, keep the raw JSON in a SUPER column so form edits never break a load, plan for the fact that deleted responses will not propagate, and tag scanned forms with their own source so they can join the same reports without being mistaken for online submissions.
