Skip to content
Enterpret Help Center home
InboxAsk a human

SFTP Integration

Updated by Vaishnavi

The SFTP integration lets you deliver feedback to Enterpret by dropping JSON, JSONL/NDJSON, CSV, TSV, or ZIP files onto an SFTP server we connect to. Enterpret polls that server on a fixed schedule, picks up new files, parses them, and processes eligible records. It's the right choice when you can't push to a webhook (batch exports, nightly jobs, vendor-provided feeds) but can produce supported export files on a schedule.

SFTP record schema: SFTP uses the flat REVIEW record schema documented below. Use Webhook Integration for structured Conversation, Survey, or Audio payloads. The Webhook metadata schema differs from SFTP.


How it works

  1. You give Enterpret a set of SFTP credentials and a remote directory.

  2. Enterpret polls that directory on a schedule.

  3. On each poll we list the directory, pick the next file in (modified-time, name) order, and stream-parse it.

  4. Eligible records are validated and ingested. When failed-record storage is configured, invalid records are uploaded to an Enterpret-managed S3 location for investigation. Upload failures can also occur. The image below illustrates records after successful processing.

    SFTP integration feedback drawer displays ingested records in the Enterpret dashboard.
  5. Once a file is fully drained, Enterpret advances its internal "watermark" so the same file is never reprocessed (unless you re-upload it with a new modified time).

We never delete or modify files on your server. Cleanup is your responsibility.


What we need from you to get set up

SFTP feedback setup form with server, authentication, remote-path, and file-type fields.

Send the following to your CSM or Enterpret point of contact. Complete the required fields shown in the setup form, including Source Type, Hostname, Port, Username, Folder Path, and authentication details. Contact your Enterpret team for assisted setup.

Field

Description

Example

Source type

A short label that identifies this feed downstream (used for filtering in the dashboard). Free-form.

Trustvoice, NPS-Q1, Custom

Hostname

SFTP server hostname or IP.

sftp.acme.com

Port

Required SFTP port. Use 22 unless your server uses a different port.

22

Username

The user Enterpret will log in as. We strongly recommend a dedicated read-only user.

enterpret-feed

Authentication

One of: password, PEM private key, or PPK private key. Use password authentication or a private key; an encrypted key can also require a passphrase.

(see below)

Remote directory

Absolute path on the SFTP server where you'll drop files.

/exports/feedback

SSH host key fingerprint

Captured automatically on the first successful setup connection (trust on first use). Later connections verify the stored key.

SHA256:abcd1234…

Authentication options

Choose an authentication method:

  • Password — simplest, but rotate regularly.

  • PEM private key — OpenSSH format. The corresponding public key must be in the user's ~/.ssh/authorized_keys on your server.

  • PPK private key — PuTTY format. Same authorized-keys requirement.

🔐 Encrypted private keys are supported. If your PEM or PPK is passphrase-protected, send the passphrase in the **password** field. The integration uses the same field for both SSH-password auth and key passphrase — never both at once.

Credentials are stored encrypted at rest. If you ever need to rotate, contact us — we'll update the integration in place.

Why we need the host key fingerprint

During initial setup, Enterpret captures the server’s SSH host-key fingerprint from the first successful connection (trust on first use). Later polls verify that stored fingerprint and fail if the server presents a different key. First-use capture does not independently verify the server’s identity; confirm the intended server before connecting.

You can grab the fingerprint with:

ssh-keyscan -t rsa,ed25519 sftp.acme.com 2>/dev/null | ssh-keygen -lf -

Use a trusted server-administration channel to check the SHA256:… value if your security team needs to validate the captured key.


File requirements

Supported formats

Supported file extensions are .json, .jsonl, .ndjson, .csv, .tsv, and .zip. Extension matching is case-insensitive.

FormatRequired layout
JSONL / NDJSONOne complete JSON object per line. Use real line breaks between records, with no commas or wrapping array.
JSONA JSON array of record objects, or newline-delimited JSON objects.
CSV / TSVUTF-8 text with a header row. Use comma-separated CSV or tab-separated TSV. Required headers are id, type, text, and either timestamp or createdAt. Header names are case-sensitive. Other nonempty columns become string metadata.
ZIPAn unencrypted ZIP containing any mix of the supported JSON, JSONL/NDJSON, CSV, and TSV data files. Other member formats are ignored.

A minimal JSONL or NDJSON file:

{"id":"review-12345","type":"REVIEW","timestamp":1714838400,"text":"Loved it"} {"id":"review-12346","type":"REVIEW","timestamp":1714838460,"text":"Confusing onboarding"}

A minimal CSV file:

id,type,text,timestamp,plan review-12345,REVIEW,Loved it,1714838400,Enterprise review-12346,REVIEW,Confusing onboarding,1714838460,Free

CSV and TSV support quoted fields, including embedded line breaks, UTF-8 BOMs, and CRLF line endings. Keep a consistent field count. Duplicate or empty headers, missing required headers, invalid UTF-8, or broken quoting can fail the file. XLSX, gzip, XML, Parquet, and plain .txt files are not supported.

File-size limits

Limit

Value

Maximum file size

2 GiB

Maximum JSONL line / CSV or TSV row length

10 MiB per line

Files over 2 GiB are skipped (we log a warning and advance past them so they don't block newer files). JSONL/NDJSON lines over 10 MiB are recorded as parse failures and processing continues. CSV/TSV logical rows are also limited to 10 MiB; an oversized row fails that file. ZIP processing also applies decompressed-size and compression-ratio safety limits. If you need to ingest more than 2 GiB at once, split it across multiple files.

Atomic uploads — the most important rule

⚠️ Never read your file before it's fully written. Enterpret skips any file whose modification time is less than 10 minutes old. This is intentional: it gives your upload time to finish.

The safe pattern is:

  1. Write your file to a temporary name (e.g. feedback_2026-05-04.jsonl.tmp).

  2. When the write is fully complete, rename it to the final name (feedback_2026-05-04.jsonl).

Use an atomic rename supported by your SFTP server within the same filesystem after the upload finishes. This reduces the risk of Enterpret reading an incomplete file. Confirm your server’s rename semantics and avoid streaming directly to the final eligible filename.

If your upload tool doesn't support atomic renames, write under one of the in-flight extensions we ignore (`.tmp`, `.partial`, `.swp`, `.lock`, `.crdownload`) and rename to the chosen supported extension only after the write completes.

Files we ignore (hidden / temp / partial)

We skip any file whose name:

  • Starts with . (e.g. .DS_Store, .staging.jsonl)

  • Starts with ~ (e.g. ~$open.jsonl)

  • Ends with .tmp, .partial, .swp, .lock, or .crdownload (case-insensitive)

Use any of these as in-flight extensions if your upload tool doesn't do atomic renames.

File ordering and naming

Files are processed in (modified-time ascending, then path ascending) order. So:

  • A file uploaded earlier always wins over one uploaded later.

  • Two files with the same mtime are processed alphabetically by path.

We recommend including a timestamp in your filename (feedback_2026-05-04T14-00.jsonl) for easy troubleshooting, but it's not what we use to order — only the mtime + path is.


Record schema

A record object in JSONL, NDJSON, or JSON uses this schema. CSV/TSV map the required columns to the same fields:

{ "id": "review-12345", "type": "REVIEW", "timestamp": 1714838400, "text": "Loved the new export feature, but onboarding was confusing.", "metadata": [ { "name": "rating", "value": "4" }, { "name": "country", "value": "US" }, { "name": "tags", "value": "feature-request" }, { "name": "tags", "value": "onboarding" } ] }

(For JSONL/NDJSON, use one object per line with no commas or wrapping array. A .json file may instead contain an array of these objects.)

Required fields

Field

Type

Notes

id

string

Unique per record. If you send the same id twice (in the same or a later file), the second one is treated as an update of the first. Use a stable, deterministic ID — review/survey row ID, not a random UUID generated at upload time.

type

string

Must be exactly "REVIEW". Other types (CONVERSATION, SURVEY, AUDIO_RECORDING) are not supported on SFTP today — they require structured payload fields the flat SFTP shape can't carry. Use the Webhook integration for those.

timestamp or createdAt

number or string

Unix epoch (seconds, ms, μs, or ns — auto-detected by magnitude) or an ISO date in YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, RFC 3339, or MM/DD/YYYY. Records with a missing or unparseable timestamp are dropped.

text

string

The customer feedback. **Must be non-empty after trimming whitespace** — empty-text records are written to the failed-records bucket

Optional fields

Field

Type

Notes

metadata

array of {name, value}

Structured properties — see below.

Metadata format and gotchas

Metadata is delivered as an array of {name, value} objects, not a flat map. This lets you send the same key multiple times (multi-select tags, multi-line addresses).

"metadata": [ { "name": "rating", "value": "4" }, { "name": "country", "value": "US" }, { "name": "tags", "value": "feature-request" }, { "name": "tags", "value": "onboarding" } ]

Behavior:

  • A key that appears once is stored as a single string.

  • A key that appears multiple times is stored as an array, in the order it appears in the file.

  • Entries with an empty or missing name are silently skipped.

  • Send metadata values as strings. Numeric values are also accepted and represented as strings; encode booleans explicitly as "true" or "false". Objects and arrays are not supported as metadata values.

  • Metadata keys are promoted to top-level filterable fields in the Enterpret dashboard, so make them human-readable (country, not c1).

ℹ️ Unlike the webhook schema, SFTP metadata does not use "s"/"n"/"b" type indicators. Just send {name, value} pairs.


What happens when something goes wrong

We separate failures into three categories, each with very different behavior. Understanding the difference is the key to diagnosing problems.

1. Connection failures (poll-level)

These prevent us from connecting to your server at all, or from listing the directory. The whole poll fails and no progress is made — we'll retry on the next scheduled poll.

Error you'll see in the integration health dashboard

What it means

What to do

connect: dial tcp ...: i/o timeout

We couldn't open a TCP connection.

Check that your server is up, on the right port, and reachable from Enterpret's IP range. Confirm any firewall / VPN allow-list.

connect: ssh: handshake failed: ssh: unable to authenticate

TCP works, SSH auth failed.

Verify the username, password, or private key. Check ~/.ssh/authorized_keys on the server. If you rotated credentials, send us the new ones.

connect: ssh: handshake failed: knownhosts: key mismatch

The server presented a different SSH host key than the fingerprint we have on file.

Either your server's host key was rotated (legitimate — re-share the new fingerprint with us) or the connection is being intercepted. Don't reuse — investigate.

no host key verification material: Polling has neither a stored fingerprint nor a configured trusted host-key source. Ask your Enterpret team to validate the server and repair the integration’s verification configuration before retrying.

list files: permission denied

We connected and authenticated, but can't read the remote directory.

chmod the directory and parent path so the SFTP user can Read and List.

2. File-level failures

These happen when we can read the directory but can't process a specific file. The file is skipped or aborted, and the watermark behavior depends on the failure type.

Failure

What we do

Watermark behavior

File over 2 GiB

Skip with a warning.

Advances past the file so it doesn't block newer files.

Unsupported extension (anything other than .json, .jsonl, .ndjson, .csv, .tsv, or .zip)

Silently skip.

Skipped entirely — use one of the supported extensions.

Hidden / temp file

Silently skip.

Same as above.

File mtime is within 10 min stability window

Skip this poll.

Does not advance — we'll reconsider it on the next poll once it's settled.

3. Record-level failures

These happen when individual records fail validation. For JSONL/NDJSON, malformed lines are recorded and processing continues. For CSV/TSV, a row with the wrong field count can be recorded and skipped, but broken quoting, invalid UTF-8, or oversized rows fail the file. Malformed JSON arrays or ZIP files can also halt file processing. Correct file-level errors before redelivering the export.

Failure

Reason saved on the record

Missing or empty id

missing or empty id

Missing/unparseable timestamp and createdAt

missing or invalid timestamp (or createdAt)

Missing type

missing or empty type

Missing or whitespace-only

missing or empty text

type is not REVIEW

unsupported type for SFTP: <X> (only REVIEW is supported)

Line is malformed JSON (broken syntax, trailing junk after the object)

_parse_error: <decoder message>, plus the raw bytes under _raw

Line is the literal null

JSON null is not a valid record object

Line exceeds the 10 MiB cap

JSONL line exceeds 10485760-byte cap (the rest of the line is drained so subsequent lines still parse correctly)

Where to find failed records: Failed records are bundled into a JSON blob and uploaded to an Enterpret-managed S3 bucket under:

sftp/<integration-id>/failed/<DD-MM-YYYY>/<file-name>_<unix-ts>_failed.json

Ask your CSM for a presigned URL or a copy of the blob if you need to debug a specific batch. The blob contains the raw record JSON annotated with _validation_error (or _parse_error + _raw for hard parse failures), so you can see exactly which fields tripped each record.

ℹ️ Failed records are not retried automatically. If you fix the data, re-upload the file (or just the corrected lines) — Enterpret-side deduplication is keyed off id, so re-sending the same id will overwrite the previous version.


Polling, watermarks, and resumability

You don't normally need to think about this — but if a file ever looks "stuck" or duplicate, here's the model:

  • We track an internal cursor of (last_modified_epoch, last_processed_path). After each successful file we advance both.

  • Files with mtime older than the cursor's epoch are skipped.

  • Files with mtime equal to the cursor's epoch are skipped if the path is alphabetically <= the cursor's path. (Same-mtime files are processed in path order.)

  • Files with mtime newer than the cursor are eligible.

What this means for you:

  • A file you re-upload gets a new mtime, so we'll reprocess it. Records with the same id will overwrite previous versions, not create duplicates.

  • A file you backdate (touch with an older mtime) may be skipped if its mtime is now older than the cursor. Don't manually backdate files unless you also reset the integration with us.

  • Large files are processed in batches. A fetch emits at most 2,000 records and saves a resume cursor. JSONL/NDJSON and CSV/TSV use byte offsets; JSON and ZIP use record-count resume. Large files can therefore take longer to finish.

  • Mid-fetch re-uploads are safe but wasteful. If you overwrite a file while we're streaming it, we'll detect that the mtime changed, discard our resume cursor, and start that file over from the beginning on the next poll. Records already ingested are deduplicated downstream, so you won't get duplicates — but you will burn extra polls.


Limits at a glance

Limit

Value

Max file size

2 GiB

Max JSONL line / CSV or TSV row length

10 MiB

Records emitted per poll

2,000 (large files paginate across polls)

File stability window

10 minutes (mtime must be at least this old to be picked up)

Default polling cadence

4 hours by default; confirm your configured schedule with your CSM

Supported extension

.json, .jsonl, .ndjson, .csv, .tsv, .zip

Supported record types

REVIEW only


Troubleshooting checklist

If a file isn't being ingested, run through this list before contacting support:

  1. Is the file in the configured remote directory? Subdirectories are not crawled.

  2. Does the filename use a supported extension? Use .json, .jsonl, .ndjson, .csv, .tsv, or .zip. Case does not matter.

  3. Does the filename start with . or ~, or end in .tmp, .partial, .swp, .lock, .crdownload? Those are ignored on purpose.

  4. Is the file's modification time at least 10 minutes ago? stat it on the server. If you just uploaded it, wait a poll cycle.

  5. Does the content match its format? JSONL/NDJSON use one object per line; JSON may use an array; CSV/TSV require the correct header and consistently quoted rows. ZIP members must use supported formats.

  6. Keep each JSONL line or CSV/TSV logical row within 10 MiB. Split large exports across files below 2 GiB.

  7. If records are failing validation, ask for the failed-records blob to see the exact reason per record.


Frequently asked questions

How quickly will my data appear in Enterpret?

The current default polling interval is four hours, and files normally must be at least 10 minutes old before they are eligible. Your connection can use a different configured schedule. Large files are fetched in batches, and processing adds time; the polling interval does not guarantee when feedback appears.

Can I send updates / corrections?

Yes. Re-upload a record with the same id and the new content; the previous version is overwritten downstream. Use a new file (or the same file with a new mtime) so we re-read it.

Can I delete records via SFTP?

Not via the SFTP path. Contact your CSM for record deletion / GDPR requests.

Will Enterpret delete or move my files after processing?

No. Files stay where you put them. You're responsible for cleanup. We recommend a daily/weekly rotation: move processed files to a sibling archive/ directory (which we don't read) once you're confident they've been ingested.

Can I have multiple SFTP integrations?

Yes. Each integration has its own credentials, remote directory, and source-type label, and is polled independently.

What's the difference between SFTP and the Webhook integration?
  • Webhook: real-time push, you call our API. Best for live data and small batches.

  • SFTP: scheduled pull, we read your files. Best for batch exports, vendor feeds, and anything that already produces files.

SFTP accepts the REVIEW schema in this guide. Do not send the complete Webhook request wrapper or assume the Webhook metadata format applies.

Which file formats can I use?

Use JSON, JSONL/NDJSON, CSV, TSV, or ZIP files that follow the layouts and limits above. JSONL/NDJSON and CSV/TSV can continue from saved byte boundaries across polls; JSON and ZIP use record-count resume. Choose a supported format your export tool can produce, and finish uploading the file before Enterpret processes it.

Can I send CONVERSATION, SURVEY, AUDIO_RECORDING, or FORUM_CONVERSATION_THREAD records via SFTP?

Not today. Those record types require structured fields (multi-message conversations, Q&A pairs, audio URLs) that the flat SFTP record shape doesn't carry. Use the Webhook integration for those.

My SFTP server's host key changed — what do I do?

Until self-serve key rotation is available, delete and recreate the integration to pick up a new host key. The fingerprint is captured automatically on first successful connect (TOFU).

Note: Recreating resets the watermark, so existing files may be reprocessed (no duplicates downstream due to ID dedupe, but it may consume extra poll cycles).

If preserving watermark state is critical, contact your CSM—engineering can update the stored fingerprint manually.

Can I use a non-standard port?

Yes — share the port number when you set up the integration.


If you run into anything not covered here, message your CSM with: the integration name, the filename in question, the time you uploaded it, and (if available) the failure message from the integration dashboard. These details help Support investigate the affected file.