Skip to main content

CSV Format for SFTP Data Integrations

Written by Ryley White

This guide defines the file delivery and CSV formatting requirements for sending operational data to Solink via SFTP and applies to all supported schemas:

  • Retail

  • Restaurant

  • Chargeback

  • Banking

  • Access Control

This guide is intended for customers and partners building an integration.


Data Ingestion Overview

Figure 1: SFTP CSV data ingestion to Solink VD (high-level)


Scope and Supported Schemas

Solink ingests CSV files delivered over SFTP and processes them according to the schema indicated in the file content (typically via a dataType column).

Schema-specific field definitions are documented separately in the companion reference: “CSV Schemas for SFTP Data Integrations”.

Retail

  • Files Expected: 3 files per day; header, item, payment

  • Use Case: Event-level retail transactional data paired with video

Restaurant

  • Files Expected: 3 files per day; header, item, payment

  • Use Case: Event-level restaurant transactional data paired with video

Banking

  • Files Expected: 1 file per day

  • Use Case: Event-level ATM / Teller transactional data paired with video


Quick Start Checklist

  • Generate CSV files in UTF-8 encoding with a header row.

  • Include only one business day per file (or per file group, depending on schema).

  • Prefix filenames with the date for automated discovery (YYYYMMDD or YYYYMMDDTHHmm).

  • Split file groups by location/store (not by line count) and keep each daily group under 50 MB.

  • Use ISO-8601/RFC 3339 timestamps with timezone (Z or ±HH:MM) for time synchronization.

  • Ensure required identifiers are stable (storeId/locationId/terminalId) and unique keys do not collide.


File Delivery Requirements

These requirements apply to all schema types unless the schema-specific documentation states otherwise.

  • Automated delivery of files over SFTP using the credentials provided by Solink.

  • Each file must contain data for a single day. Do not mix multiple business days in one file.

  • Do not split a single day’s transactions across multiple, overlapping uploads. A transaction/event must appear in only one daily group.

  • Split large deliveries into multiple files and add a numeric suffix (for example, 20240213-header-2.csv).

  • Split by location/store when possible. Avoid splitting by line count unless required by file size limits.

  • Keep each daily group (all files for a day, per schema and per location strategy) at or below 50 MB.


CSV Formatting Requirements

Solink expects CSV files to follow standard conventions for comma-separated values, including quoting rules for fields containing commas, quotes, or line breaks.

  • Encoding: UTF-8.

  • Delimiter: comma (,).

  • Header row: first row must contain column headers.

  • Quoting: use double quotes (") around fields containing special characters (comma, CR/LF, or quotes). To include a quote inside a quoted field, escape it by doubling it (""), per common CSV conventions.

  • Line endings: LF or CRLF are accepted; be consistent within a file.

  • Empty values: represent missing optional values as empty fields; do not shift columns, and do not include NULL as a value.


File Naming Conventions

File names are used for automated discovery and to group related files. Use the schema-specific fileType values where applicable.

Component

Required

Format

Examples

<datestamp>

Yes

YYYYMMDD (daily) or YYYYMMDDTHHmm (multiple drops/day)

20240213, 20240213T1233

<storeId>

No

Alphanumeric identifier for a physical location/store

A203, store535

<fileType>

Schema-specific

One of the file types listed in the schema section

header, item, payment, chargeback, accesscontrol


Timestamp Requirements (ISO-8601 / RFC 3339)

Solink synchronizes events to video using absolute time. Use ISO-8601 timestamps with an explicit timezone designator (Z for UTC or an offset such as -05:00). Include milliseconds when possible to improve alignment.

  • 2024-06-15T06:15:00.000-06:00 (local time with offset)

  • 2024-06-15T12:15:00.000Z (UTC)

Tip: Avoid ambiguous local timestamps (no timezone). If your source system cannot emit offsets, export in UTC and use the Z suffix.

Format pattern:

YYYY-MM-DDTHH:mm:ss.sss±HH:MM (with timezone offset)

YYYY-MM-DDTHH:mm:ss.sssZ (UTC)

Examples:

2024-08-02T17:09:04.818Z → Aug 2, 2024 5:09:04 PM UTC

2024-06-15T06:15:00.000-06:00 → Jun 15, 2024 6:15:00 AM CST

2024-12-30T00:06:31.070Z → Dec 30, 2024 12:06:31 AM UTC


Validation and Common Rejection Reasons

During ingestion, Solink validates file structure and schema requirements. Rows (or entire files) may be rejected if they do not conform.

  • Missing required columns or empty required fields.

  • Invalid enum/picklist values (for example, Access Control result/state).

  • Non-unique identifiers where uniqueness is required (for example, transactionID in Retail / Restaurant). Duplicate IDs overwrite earlier transactions. ID’s must be unique across all stores, registers, and days.

  • Invalid timestamp format or timestamps without timezone.

  • itemTime or paymentTime is outside the transaction window. The header startTime and endTime sets this window.

  • Overlapping transactions/events across multiple daily groups.

  • TransactionId / SequenceID cannot contain any of the following characters: ! ' ( ) ; : @ & = + $ , / ? % # [ ]


Data Privacy and Minimization

Send only the data required for the integration. When customer identifiers are needed, use redacted or tokenized identifiers where possible (for example, last four digits rather than full account or card numbers).


Use Case Handling

This section explains how to send specific file types to Solink.

Examples below show only the relevant columns. Required columns such as dataType, storeId, and startTime still apply.

The Balance Rule

Every transactionId must satisfy the following across the three files:

sum(line totals) - transactionDiscount or 
sum(itemdiscount) + transactionTax =
sum(totalPaymentTendered) − changeDue

A line total is (unitPrice × quantity) − itemDiscount. Sale lines add, Return and Void lines subtract.

  • Send positive amounts. Direction comes from status and itemStatus, never from a minus sign.

  • Tax goes on the header only. Use transactionTax. Do not bake tax into unitPrice.

Discounts

Send each discount in one place: itemDiscount for a line, or transactionDiscount for the whole receipt. Never both for the same dollars. Solink will subtract both.

Example: $100.00 jacket, $20.00 off, $8 tax (10%) on the discounted $80.00:

# item
transactionId,itemStatus,quantity,unitPrice,itemDiscount
1001,Sale,1,100.00,20.00
# header
transactionId,status,transactionDiscount,transactionTax
1001,sale,0.00,8.00
# payment
transactionId,tenderType,totalPaymentTendered,changeDue
1001,"Credit VISA",88.00,0.00

Check: (100.00 − 20.00) + 8.00 = 88.00

For a receipt-level discount, put the amount in transactionDiscount instead and leave itemDiscount at 0.00.

Free, comped, or 100% discounted lines must net to zero. Never discount against a 0.00 price:

  • Correct: unitPrice 8.00, itemDiscount 8.00 → line total 0.00

  • Incorrect: unitPrice 0.00, itemDiscount 8.00 → line total −8.00

Refunds

Header status = refund, every line itemStatus = Return, all amounts positive. totalPaymentTendered is the amount returned to the customer and changeDue is 0.00. Link back with originalTransactionId.

Example: The $88.00 jacket sale returned in full:

# item
transactionId,itemStatus,quantity,unitPrice,itemDiscount,originalTransactionId
2001,Return,1,100.00,20.00,1001
# header
transactionId,status,transactionDiscount,transactionTax
2001,refund,0.00,8.00
# payment
transactionId,tenderType,totalPaymentTendered,changeDue
2001,"Credit VISA",88.00,0.00

Check: (100.00 − 20.00) + 8.00 = 88.00

Partial Refund

Header status = sale, itemStatus = return+sale(mix).

Use status = sale here because the customer still pays. All amounts stay positive. changeDue = 0.00.

Example: Customer keeps shoes $50.00, returns socks $30.00. 10% tax on the net $20.00 = $2.00. The customer pays $22.00:

# item
transactionId,itemStatus,quantity,unitPrice,itemDiscount,originalTransactionId
2002,Sale,1,50.00,0.00,
2002,Return,1,30.00,0.00,1002
# header
transactionId,status,transactionDiscount,transactionTax
2002,sale,0.00,2.00
# payment
transactionId,tenderType,totalPaymentTendered,changeDue
2002,"Credit VISA",22.00,0.00

Check: 50.00 − 30.00 + 2.00 − 0.00 = 22.00 − 0.00

Full Voided Transactions

Example: A $100.00 jacket with a $20.00 discount is rung, then the whole ticket is cancelled before the customer pays. Header status is void, every line is Void, transactionTax is 0.00, and there is no payment row (no matching Sale is required):

# itemfile
transactionId,itemStatus,quantity,unitPrice,itemDiscount
1008,Void,1,100.00,20.00
# headerfile
transactionId,status,transactionDiscount,transactionTax
1008,void,0.00,0.00
# paymentfile
transactionId,tenderType,totalPaymentTendered,changeDue
(no rows)

Partial Voided Transactions

itemStatus = Void +sales mix.

Example: A $25.00 item is rung up, voided, and the sale completes with a $10.00 item at 10% tax. Void lines subtract from the item total, so a matching $25.00 Sale is required. Without it the total would be $10.00 − $25.00 instead of $10.00, and it would not match the $11.00 payment:

# itemfile
transactionId,itemStatus,quantity,unitPrice
1003,Sale,1,25.00
1003,Void,1,25.00
1003,Sale,1,10.00
# headerfile
transactionId,status,transactionTax
1003,sale,1.00
# paymentfile
transactionId,tenderType,totalPaymentTendered,changeDue
1003,Cash,11.00,0.00

Check: (25.00 − 25.00 + 10.00) + 1.00 = 11.00

Exchanges

Example: Exchange, header is sale, and on the receipt a Return and a Sale line that cancel, transactionTax of 0.00, and no payment row:

# item
transactionId,itemStatus,quantity,unitPrice,originalTransactionId
3001,Return,1,45.00,1004
3001,Sale,1,45.00,
# header
transactionId,status,transactionTax
3001,sale,0.00
# payment
(no rows)

Check: (45.00 − 45.00) + 0.00 = 0.00

Split Tender

One payment row per tender. The sum of totalPaymentTendered minus changeDue must equal the transaction total. Keep paymentTime between the header's startTime and endTime.

Example: an $88.00 purchase paid with a $50.00 credit card and $60.00 cash:

# payment
transactionId,tenderType,totalPaymentTendered,changeDue
1006,"credit Card",50.00,0.00
1006,Cash,60.00,22.00

Check: (50.00 + 60.00) − 22.00 = 88.00

changeDue goes on the cash row only.

Change Due (Cash Payments)

changeDue is the cash handed back. totalPaymentTendered on that row is what the customer handed over, before change.

Example: $43.75 purchase paid with $50.00 cash:

# payment
transactionId,tenderType,totalPaymentTendered,changeDue
1007,Cash,50.00,6.25

Check: 50.00 − 6.25 = 43.75

  • Cash tendered above the total: changeDue is the actual change, positive

  • Split tender with cash overpayment: changeDue on the cash row only

Never send changeDue equal to the full cash tender, a negative changeDue.

Did this answer your question?