Data export API
Pull your raw response data programmatically as JSON or CSV. Pro plan.
The data export API lets you pull your raw response data as JSON or CSV from your own scripts, a scheduled job, or a BI tool. It is read-only, server-to-server, and returns the same data as the CSV export in the admin.
The data export API requires the Pro plan. On Free and Growth you can still export everything to CSV by hand from the Responses page.
Authentication
Every request is authenticated with your store's own API key, sent as a bearer token.
Generate your key
In the Feedloop admin, open Settings and find the API access section. A few things to know:
- The key is shown once, at the moment you generate it. Copy it straight away, because Feedloop stores only a hash of it and cannot show it to you again.
- The key starts with
fdlp_followed by 48 hexadecimal characters. - Generating a new key replaces the old one. The previous key stops working immediately, which is also how you revoke a key you think has leaked.
- If your store drops below the Pro plan the key stops working, and starts working again if you return to Pro. You do not need to regenerate it.
Send it on every request
Put the key in the Authorization header as a bearer token:
Authorization: Bearer fdlp_your_key_here
Never put the key in a URL or in client-side code. It grants read access to all of your response data.
Endpoint
GET https://feedloop.saliapps.com/api/v1/export/responses
Quick start
curl -s https://feedloop.saliapps.com/api/v1/export/responses \
-H "Authorization: Bearer fdlp_your_key_here"
That returns the first page of responses as JSON. The examples further down add date filtering, CSV output, and pagination.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
since |
ISO 8601 date or datetime | none | Only responses submitted at or after this time, for example 2026-07-01 or 2026-07-01T00:00:00Z. |
format |
json or csv |
json |
The response encoding. |
limit |
integer, 1 to 250 | 250 |
Page size. Values above 250 are capped at 250. |
cursor |
response id | none | Return the page that follows this response id. Pass the nextCursor from the previous page. |
Responses are always ordered by ascending id, which is what keeps the cursor stable as new responses arrive.
JSON response
{
"responses": [
{
"id": "cms7a5af6000nvln4ogvkij0w",
"submittedAt": "2026-07-28T13:57:01.705Z",
"survey": "Post-purchase NPS",
"source": "THANK_YOU",
"npsScore": 1,
"orderId": "5079105710",
"recoveryCode": "FEEDLOOP-A1B2C3",
"answers": [
{ "question": "How likely are you to recommend us?", "type": "NPS", "value": "1" },
{ "question": "What let us down?", "type": "TEXT", "value": "Shipping was slow" }
]
}
],
"nextCursor": "cms7a5af6000nvln4ogvkij0w"
}
Fields
| Field | Type | Notes |
|---|---|---|
id |
string | The response id. This is also the pagination cursor. |
submittedAt |
string | ISO 8601 timestamp of the submission. |
survey |
string | The survey's name. |
source |
string | Where it was answered: THANK_YOU, ORDER_STATUS, or THEME_EMBED (the on-site pop-up). |
npsScore |
integer or null | 0 to 10, or null if the survey had no NPS question. |
orderId |
string or null | The Shopify order id, or null for anonymous pop-up responses. |
recoveryCode |
string or null | The detractor discount code, if one was created for this response. |
answers |
array | One entry per answered question. |
answers[].question |
string | The question prompt. |
answers[].type |
string | NPS, SINGLE_CHOICE, MULTI_CHOICE, or TEXT. |
answers[].value |
string | The answer. Multiple-choice selections are joined with a comma and a space. |
nextCursor |
string or null | Top level. The id to pass as cursor for the next page, or null when there are no more pages. |
CSV response
Add format=csv to get a flat file instead of JSON:
curl -s "https://feedloop.saliapps.com/api/v1/export/responses?since=2026-07-01&format=csv" \
-H "Authorization: Bearer fdlp_your_key_here" \
-o responses.csv
The columns are:
response_id,submitted_at,survey,source,nps_score,order_id,question,answer,recovery_code
There is one row per answer, so a response with three answers spans three rows that share the same response_id. A response with no answers still emits one row, with the question and answer columns left blank, so nothing silently disappears from the file.
Pagination
Each page returns up to limit responses (250 by default) and a nextCursor. Pass that cursor back to get the next page. When nextCursor is null, you have reached the end.
Here is a full loop in bash that walks every page, using jq to read the cursor:
KEY="fdlp_your_key_here"
BASE="https://feedloop.saliapps.com/api/v1/export/responses"
cursor=""
while true; do
url="$BASE?limit=250"
if [ -n "$cursor" ]; then url="$url&cursor=$cursor"; fi
page=$(curl -s "$url" -H "Authorization: Bearer $KEY")
echo "$page" | jq -c '.responses[]'
cursor=$(echo "$page" | jq -r '.nextCursor // empty')
if [ -z "$cursor" ]; then break; fi
done
Rate limits
The endpoint allows up to 60 requests per 60 seconds per store. Beyond that it returns 429 rate_limited; wait a moment and retry. At a page size of 250 that is up to 15,000 responses a minute, far more than any single export needs.
Errors
Errors return the matching HTTP status and a JSON body shaped like { "error": { "code", "message" } }.
| Status | code |
When |
|---|---|---|
| 400 | validation_error |
since is not a valid ISO 8601 date. |
| 401 | auth_error |
The Authorization header is missing or malformed, the key is unknown or was revoked, or the store is not on Pro. |
| 429 | rate_limited |
More than 60 requests in 60 seconds. |
| 500 | internal_error |
An unexpected server error. |
{ "error": { "code": "auth_error", "message": "The data export API requires the Pro plan" } }