> ## Documentation Index
> Fetch the complete documentation index at: https://docs.solo.one/llms.txt
> Use this file to discover all available pages before exploring further.

# SFTP Getting Started

> Send your first JSONL file to the SOLO Network over SFTP

This guide walks you through your first SFTP upload end-to-end — from getting credentials to confirming that the network ingested your data. Plan on about 15 minutes.

## What you'll need

<Steps>
  <Step title="A WorkOS API key for your organization">
    SFTP authentication is delegated to WorkOS. Your SOLO account manager will help you (or your WorkOS admin) mint a key under your organization. The key string itself is the SFTP password.

    <Warning>
      Treat the API key like any other credential. Store it in a secrets manager and never commit it to source control. The key — not the username — is what grants access: anyone holding it can upload as your organization.
    </Warning>
  </Step>

  <Step title="The email you use to sign in to SOLO">
    Your email is the SFTP username. It identifies *who* is connecting and selects the environment via an optional plus-tag. The organization your uploads are attributed to is determined by the **API key's owning organization**, not by the email — so a valid key is required no matter what username you present.
  </Step>

  <Step title="An SFTP client">
    Anything that speaks SFTP works — the `sftp` command from OpenSSH, [FileZilla](https://filezilla-project.org/), [Cyberduck](https://cyberduck.io/), or a library such as Python's [Paramiko](https://www.paramiko.org/).
  </Step>
</Steps>

## Step 1 — Pick an environment

We recommend doing your first upload against **sandbox**. Sandbox is a fully isolated copy of the network: nothing you upload there reaches production data, so it's the right place to learn the format and verify your pipeline.

You select an environment by appending a plus-tag to the local part of your email:

| Environment | Username Format | Example |
| - | - | - |
| Sandbox | `email+sandbox@domain.com` | `alice+sandbox@yourbank.com` |
| Production | `email@domain.com` | `alice@yourbank.com` |

Each environment writes to its own storage bucket and database; there is no crossover. Usernames are case-insensitive — `Alice@YourBank.com` and `alice@yourbank.com` are the same user.

## Step 2 — Connect

The SFTP host is the same for every environment:

```
sftp.solo.one:22
```

<CodeGroup>
  ```bash sftp (CLI) theme={null}
  sftp alice+sandbox@yourbank.com@sftp.solo.one
  # When prompted, paste your WorkOS API key as the password.
  ```

  ```python Python (paramiko) theme={null}
  import paramiko

  transport = paramiko.Transport(("sftp.solo.one", 22))
  transport.connect(
      username="alice+sandbox@yourbank.com",
      password="sk_live_your_workos_api_key",
  )
  sftp = paramiko.SFTPClient.from_transport(transport)
  # ... upload here ...
  sftp.close()
  transport.close()
  ```

  ```text ~/.ssh/config theme={null}
  Host solo-sftp-sandbox
      HostName sftp.solo.one
      User alice+sandbox@yourbank.com
      Port 22
  ```
</CodeGroup>

On a successful connection you'll land at the root of your organization's upload area.

<Warning>
  Your SFTP session is **upload-only**: you can `put` files and create directories, but `ls`, `get`, overwriting, and deleting are not permitted. Don't be surprised when a directory listing is denied — that's policy, not a fault. Verification happens in the dashboard (Step 5), not over SFTP.
</Warning>

<Tip>
  If the server rejects you immediately, the most common causes are (1) the API key has been revoked, (2) your organization isn't provisioned in SOLO yet, or (3) the plus-tag is misspelled (only `+sandbox`, `+prod`, or no tag are accepted). Re-check the username and key, then reach out to your account manager if it persists.
</Tip>

## Step 3 — Write a JSONL file

Every upload is a [JSONL file](/api-overview/furnishing/file-upload): UTF-8 text with one JSON object per line. Each record carries a `_type` naming the kind of record it is — that's how the network decides which mappings receive it.

Find the record type in the dashboard: open **Feeds**, pick the feed you're delivering, and read its sample record. The sample's `_type` is the value to use, and its other fields are the ones the feed's mapping reads.

For this walkthrough, write one consumer record for a feed whose record type is `consumers`:

```json first_upload.jsonl theme={null}
{"_type":"consumers","ssn":"123456789","dob":"1990-01-15","first_name":"Jane","last_name":"Doe"}
```

A few things to know about the format ([full reference](/api-overview/furnishing/file-upload#file-format)):

* **One compact object per line** — not a JSON array, and not pretty-printed across lines.
* **Blank lines are skipped.** Every other line is processed independently, so a single bad line won't fail the rest of the file.
* **Identifiers are strings.** Write SSNs and tax identifiers as `"012345678"`, never as numbers — a number drops the leading zero.
* **Filenames** use letters, digits, `_`, and `-`, with a single `.jsonl` extension.

## Step 4 — Upload

Drop the file into `network_{network_id}/jsonl_furnish_data/`, where `{network_id}` is the network the records go to. The feed's detail page in the dashboard shows the network id in its drop path. Your session is already scoped to your organization, so leave off the drop path's leading `entity_…/` segment:

<CodeGroup>
  ```bash sftp (CLI) theme={null}
  sftp> mkdir network_9f1c0c2e-7a44-4f1e-9d2a-08b6f3a1c5d7
  sftp> cd network_9f1c0c2e-7a44-4f1e-9d2a-08b6f3a1c5d7
  sftp> mkdir jsonl_furnish_data
  sftp> cd jsonl_furnish_data
  sftp> put first_upload.jsonl
  sftp> bye
  ```

  ```python Python (paramiko) theme={null}
  network_dir = "network_9f1c0c2e-7a44-4f1e-9d2a-08b6f3a1c5d7"
  for directory in (network_dir, f"{network_dir}/jsonl_furnish_data"):
      try:
          sftp.mkdir(directory)
      except OSError:
          pass  # already exists
  sftp.put("first_upload.jsonl", f"/{network_dir}/jsonl_furnish_data/first_upload.jsonl", confirm=False)
  ```
</CodeGroup>

<Tip>
  Pick a fresh filename for every upload (a date or batch number works well). Sessions can't overwrite or delete existing files, so re-using a name from a previous drop will be rejected. Pass `confirm=False` in Paramiko — its post-upload check lists the file, which upload-only sessions don't allow.
</Tip>

## Step 5 — Verify ingestion

Ingestion is event-driven and asynchronous — processing starts within seconds of the file landing, and a small file typically completes in well under a minute.

The cleanest way to check is the [SOLO dashboard](https://app.solo.one): the **Uploads** page lists every file your organization has sent, with the records read and the succeeded, filtered, failed, and unmatched counts. The feed's detail page shows its recent deliveries and names any record types no mapping claimed.

<Note>
  Re-sending data is safe. Identity records resolve to the existing consumer or business on their natural keys (SSN and date of birth for consumers; tax identifier and jurisdiction for businesses), so a corrected re-upload attaches to the same profiles instead of creating duplicates.
</Note>

## Common first-upload issues

<AccordionGroup>
  <Accordion title="Authentication failed / Permission denied at login">
    Confirm the username is your full email address (with `+sandbox` for sandbox), and the password is the literal WorkOS API key string — no `Bearer` prefix, no quotes. If you rotated the key recently, the old value is rejected; a freshly revoked key may take up to a minute to be refused everywhere, since auth results are briefly cached.
  </Accordion>

  <Accordion title="&#x22;Permission denied&#x22; on ls, get, or overwrite">
    Working as intended — SFTP access is upload-only. You can `put` new files and create directories; everything else is denied. Use the dashboard to inspect what you've uploaded, and use a new filename rather than overwriting an old one.
  </Accordion>

  <Accordion title="&#x22;Host key verification failed&#x22;">
    The first time you connect from a new machine your SSH client doesn't know the server's host key yet. Either accept the fingerprint at the prompt, or pre-populate your `known_hosts`:

    ```bash theme={null}
    ssh-keyscan sftp.solo.one >> ~/.ssh/known_hosts
    ```
  </Accordion>

  <Accordion title="File uploaded but no upload appears in the dashboard">
    The file landed outside `jsonl_furnish_data/`, so it was stored but never ingested. Re-upload it into the feed's drop path.
  </Accordion>

  <Accordion title="Records are counted as unmatched">
    No active mapping in the target network claims their `_type`. Compare the value with the feed's sample record, confirm the feed's mapping is active rather than a draft, and confirm the drop path names the network you expected.
  </Accordion>

  <Accordion title="The upload fails with an access error">
    Your organization doesn't hold the furnisher or governor role in the network named by the drop path. Check the network id in the path, or ask the network's governor to add your organization as a furnisher.
  </Accordion>
</AccordionGroup>

## Where to go next

<CardGroup cols={2}>
  <Card title="SFTP Overview" icon="server" href="/api-overview/sftp/overview">
    The full mental model — directories, environments, how routing works.
  </Card>

  <Card title="JSONL Files" icon="file-arrow-up" href="/api-overview/furnishing/file-upload">
    The file format, record types, and results reference.
  </Card>

  <Card title="Furnishing Policies" icon="shield-check" href="/concepts/governance/furnishing-policies">
    How mappings turn records into network data.
  </Card>

  <Card title="API Reference" icon="webhook" href="/api-reference/introduction">
    Prefer to furnish over HTTPS? Records can be sent one at a time as REST calls.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.