Integration · Platform engineering
How to migrate an SFTP batch feed to an API
A nightly file has run for eleven years, the person who wrote it has gone, and it turns out an absent row means a deletion. The thinking behind migrating it safely, the prompts that build it, and what it takes to keep it running.
Built with Tray Headless
- System SFTP
- Step Parse and diff
- Step Run both in parallel
- Step Cut over per record type
- System NetSuite
Both feeds run together and are compared every night. The file stays authoritative until a diff has been empty across a month end.
The short answer
What does it take to migrate an SFTP batch feed to an API?
Migrating an SFTP batch feed to an API means first recovering the semantics the file carries and nobody documented, then running both paths in parallel and diffing them nightly, then cutting over one record type at a time once the differences are zero. The part that goes wrong most often is treating the file as a list of records. A full-file replace usually means an absent row is a deletion, and no per-record API call expresses that at all.
What matters here
- The file is a contract nobody wrote down. Recover its semantics before designing anything.
- A full-file replace means absence is deletion. No sequence of per-record API calls says that.
- Run both paths in parallel and diff them nightly. The diff is the specification you never had.
- Cut over one record type at a time, never the whole feed at once.
- Do not cut over until the diff has been empty across a month end. Month end is where the feed behaves differently.
- A truncated upload looks like a valid smaller file. Check the completeness marker before parsing.
Who this is for
You run platform or integration engineering. A batch file feeds something important, the API alternative has existed for years, and nobody has been willing to touch it.
How it works in practice
From a nightly file that nobody understands to an API integration somebody can change.
- 1
The file semantics are recovered
Full replace or delta, ordering, key, and what an absent row means.
- 2
Completeness is checked before parsing
A truncated upload is a valid smaller file and will be processed as one.
- 3
The API path is built to match those semantics
Including the deletions that a file expresses by omission.
- 4
Both paths run in parallel
The file stays authoritative and the API path writes to a shadow target.
- 5
The two are diffed every night
And every difference is explained, not averaged away.
- 6
Cut over per record type, after a month end
Because month end is where a batch feed behaves unlike any other day.
What the migration is made of
Four parts, and the first is archaeology rather than engineering.
Recovered semantics
Whether the file replaces or appends, what the key is, whether order matters, and what an absent row means. None of this is in a document.
Completeness checking
A trailer record, a row count, or a control file. Without one, a half-uploaded file parses cleanly and deletes half your data.
A parallel run
Both paths live, the file authoritative, the API writing to a shadow. This is the only way to learn what the file was really doing.
A nightly diff
Every difference explained. The diff is the specification the original never had, and it is worth more than any document.
The Tray Headless prompts
Paste these into Claude Code or Codex with the Tray Headless plugin installed. Each stage runs on its own. The systems named in them are the worked example rather than a requirement, and every prompt says so.
Once per project, run
/tray-workflows:set-workspace
to pick the workspace these build in. Point it at a sandbox first.
- 1
Set up and recover what the file actually means
It is a contract, and nobody wrote it down.
Headless skills
build-workflowUse build-workflow. The systems in play are NetSuite and the API that is replacing the feed, or whatever we run in those seats. Before designing anything, work out from real files what the feed actually means: Is each file a full replace of the record set, or a delta What is the key, and is it stable Does row order carry meaning What does an absent row mean: unchanged, or deleted Are there record types beyond the obvious one, distinguished by a column What happens on a day with no changes, an empty file or no file at all Take a month of files and diff them against each other. The answers are in the data, because they are not in a document and the person who knew them has left. The absent-row question is the one that matters most. If a full-file replace means absence is deletion, then a naive per-record API migration will never delete anything, and the target will accumulate records that should have gone years ago.
- 2
Check the file is complete before parsing it
A truncated upload is a valid smaller file.
Headless skills
build-workflowUse build-workflow. Before parsing anything: Look for a trailer record or control file with an expected row count, and reject a mismatch Reject a file whose size is wildly outside the recent range, which is usually a partial upload Reject a file that arrived while still being written, by requiring a marker file or a rename into place Handle no file at all as an alert, not as an empty change set A half-uploaded full-replace file is the worst case available here. It parses cleanly, contains a valid subset, and if absence means deletion it removes everything that had not uploaded yet. Record every file received with its checksum and row count, so a question about what arrived on a given night has an answer.
Reject an unexpectedly small full-replace file rather than processing it. It is nearly always a partial upload, and processing it is not recoverable.
- 3
Build the API path to the same semantics, deletions included
Absence is the part a per-record API cannot express.
Build the API path to reproduce what the file means, not what the API finds convenient: For a full replace, compute the set difference and issue explicit deletes for anything no longer present For a delta, respect ordering where the file has it Reproduce the same key, so records line up for comparison Handle the record types the file distinguishes by column value Keep the API path idempotent and keyed, so re-running a night is safe. You will re-run nights during the parallel period, repeatedly. Where the API cannot express something the file does, write that down and raise it rather than approximating. An approximation discovered during cutover is a rollback; the same approximation discovered during the parallel run is a design conversation.
- 4
Run both, diff nightly, and explain every difference
The diff is the specification you never had.
Headless skills
tray-gotchasUse tray-gotchas, then run both paths together. The file stays authoritative and writes to production; the API path writes to a shadow target. Every night, diff the two: Records in one and not the other Records present in both with different values, field by field Counts by record type Explain every difference. Do not summarise them as a match percentage. Each one is either a bug in the new path or a behaviour of the old feed nobody knew about, and both are worth the hour. Expect the interesting ones to appear at month end, on the day a record type appears that has not been seen in weeks, and on the day somebody sends a correction file. That is why the parallel period spans a month end rather than a fortnight.
- 5
Cut over one record type at a time
Because a whole-feed cutover has one rollback and no diagnosis.
Cut over per record type, and only when that type has shown zero unexplained differences across a full month including month end. Keep the file path running and diffing after cutover, with the roles reversed: the API path is authoritative and the file becomes the check. Retire the file only when a quarter has passed without a difference. Report during the migration: differences by record type and by field, days since the last unexplained difference per type, file arrival times and completeness rejections, and which types have cut over. Days since the last unexplained difference is the number that decides the cutover. A team that cuts over on schedule instead of on that number is choosing the date over the evidence.
- 6
Check it end to end, then hand the cutover to platform engineering
Because the sequence is a judgement, not a schedule.
Headless skills
tray-patternsRun the per-step schema checks and the whole-workflow audit before this touches production. Feed a deliberately truncated file into the sandbox and confirm it is rejected rather than processed. Then open the same workflow in Tray Build so platform engineering can control which record types have cut over, adjust the completeness thresholds and read the nightly diff in the visual canvas.
What it connects to
The file is on one side, the API on the other, and the diff between them is the whole point.
SFTP
Collect the file, verify its completeness marker and archive it with a checksum before anything parses it.
Reads
NetSuite
The target the file feeds today and the API path feeds tomorrow, with the same records and the same keys.
Reads and writes
Snowflake
Hold both outputs and the nightly diff, so a difference from three weeks ago can still be investigated.
Writes
Slack
Alert on a missing file, a completeness rejection or a new unexplained difference, not on the routine ones already known.
Writes
Jira
Raise each unexplained difference as work, so the cutover decision rests on a queue that is empty rather than on a date.
Writes
Same build, other stacks
The design does not change if you run something else in one of these seats. The same prompts build it against SAP S/4HANA, Google BigQuery, Microsoft Teams, ServiceNow, Oracle or Databricks.
Named systems are the ones most teams run, not the only ones that work. Each is an authentication in your Tray workspace, referenced by name, so the workflow never holds a credential. Where we have a connector page, the name links to it.
Running it in production
A feed that has run for a decade is load-bearing. Everything here is about not finding that out the hard way.
The platform runs it, not a laptop under a desk
Files arrive on their own schedule and the diff runs every night. A migration that depends on somebody remembering to compare is not a migration.
Every file received is archived with its checksum
So a question about what arrived on a night eight weeks ago has an answer, which is exactly the question a difference raises.
Credentials are managed, never written into the build
SFTP keys and API credentials held in your workspace and rotated separately, which the original script almost certainly did not do.
Platform engineering own the cutover
Which record types have moved, the completeness thresholds and the difference queue, all open in Tray Build.
The old path keeps running after cutover
With the roles reversed, so the file becomes the check. Retiring it early removes the only evidence that the new path is right.
Questions people ask
Why is migrating a batch file harder than it looks?
Because the file is a contract nobody wrote down. Whether it replaces or appends, what the key is, whether order matters and what an absent row means are all decisions somebody made years ago and never documented.
What does a full-file replace imply?
That an absent row is a deletion. No sequence of per-record API calls expresses that, so a naive migration stops deleting and the target accumulates records that should have gone.
Why run both paths in parallel?
Because the nightly diff is the specification the original never had. Every difference is either a bug in the new path or a behaviour of the old feed nobody knew about.
Why wait for a month end before cutting over?
Because a batch feed behaves differently at month end than on any other day. A parallel period that has not crossed one has not tested the interesting case.
What is the danger with a truncated upload?
It is a valid smaller file. It parses cleanly, and where absence means deletion it removes everything that had not finished uploading.
When can the file feed be retired?
After cutover it keeps running as the check, with the API authoritative. Retire it when a quarter has passed with no difference, because until then it is the only evidence the new path is right.
Related guides
Platform engineering
How to build an API facade
Give consumers one contract instead of six systems, absorb upstream changes rather than leaking them, and keep the caller identity intact. The prompts.
Platform engineering
How to build a change data capture pipeline
Read the log instead of polling a timestamp, overlap the snapshot with the stream, and keep deletes and order intact. The Headless prompts that build it.
Finance
How to build a vendor master sync
Onboard a supplier once, verify bank details out of band, keep one record per legal entity, and make a change to payment details an event. The prompts.
Last reviewed September 2026.