> For the complete documentation index, see [llms.txt](https://docs.thewalletcrew.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.thewalletcrew.io/configure/advanced-configuration/wallet/import-and-export/update-pass-using-flat-files.md).

# Update Pass Using Flat Files

Update existing Apple Wallet and Google Wallet passes in bulk from CSV files uploaded to SFTP: data, template, theme, benefits and notifications.

The Wallet Crew can update existing Apple Wallet and Google Wallet passes from a CSV file. This is useful for bulk updates and for legacy systems that can only export flat files. Each row of the file can change the data shown on a pass, switch its template or theme, grant a benefit and send a notification to the devices holding it.

{% hint style="warning" %}
SFTP imports are an optional feature. Ask The Wallet Crew to enable SFTP on the tenant before implementing this flow.

Treat SFTP imports as a **last resort**. Avoid this flow unless there is no other option (for example, a legacy system that can only export flat files).

For most projects, the API is the recommended approach. It is easier to automate, easier to monitor, and updates in real time.

For a real-time integration, use the [API update flow](/developers-guides/integration-guides/wallet/pass-lifecycle.md).
{% endhint %}

<details>

<summary><strong>Real-world examples</strong></summary>

* A gift card program recalculates balance every hour. A flat file export updates `additionalData.balance` for all active cards.
* A loyalty program upgrades members to Gold every night. One file switches each upgraded pass to the Gold theme, grants a welcome benefit and notifies the member in their language.
* A seasonal campaign ends. A file sets `themeId` to `-` on every pass that carried the campaign theme, which removes the theme from each of them.
* A ticketing system exports last-minute changes (gate, seat, or schedule). A bulk CSV update keeps tickets accurate right before doors open, and a notification tells each holder what changed.
* A gym chain exports membership status every night (active, frozen, expired). The next day, members see the correct status at check-in.
* A support team fixes a wrong identifier at scale by uploading a one-time correction file.

</details>

<div data-with-frame="true"><figure><img src="https://123508161-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FDJrf9G0l7ArnvuwEH0a8%2Fuploads%2FAlMXw1s03rRnSwC6F8AC%2Fconfigure-update-pass-using-flat-diagram-showing-csv-file-uploaded-sftp-then.png?alt=media&#x26;token=e4e1a102-e785-4bdf-885d-e1867a09db3f" alt="Diagram showing a CSV file uploaded to SFTP, then processed by The Wallet Crew to update passes."><figcaption><p>Flat file imports update many passes in one operation.</p></figcaption></figure></div>

## Process overview

1. Upload the CSV file to the provided SFTP server.
2. The Wallet Crew automatically detects new files and processes them immediately.
3. Each row is checked, then turned into an update for every pass it identifies. A row that references something the tenant does not have is refused and changes nothing; the other rows carry on.
4. Once every row has been read, the file is moved to `.processed/`. A file whose import fails is moved to `.error/`.
5. Pass updates are queued and delivered asynchronously, at a steady pace across the whole file (see [Delivery pace](#delivery-pace)).
6. The result of the import, row by row and pass by pass, is visible in the console (see [Follow an import](#follow-an-import)).

## File format

Uploaded files must be valid [RFC 4180](https://www.ietf.org/rfc/rfc4180.txt){target="\_blank"} compliant CSV files.

* **Separators**: Either `,` or `;`
* **Header row**: Mandatory. Must contain column names, each one non-empty and present only once. Column names are compared without regard to case, so `themeId` and `ThemeId` in the same header count as the same column twice.
* **Encoding**: UTF-8 recommended
* **Parser**: The Wallet Crew uses [CsvHelper](https://joshclose.github.io/CsvHelper/){target="\_blank"} with default options

A file with no header row, an empty or repeated column name, or malformed CSV (for example an unclosed quote) is rejected and moved to `.error/`. When the malformed line is found partway through the file, some of the rows before it may already have been queued; those are still applied.

Each data row targets one or more passes and can include multiple updates to their fields, additionalData, theme or metadata.

## File name

There are no strict file name requirements. However, for better organization and traceability, we recommend the following naming convention:

```
<source>-passes-<YYYYMMDD>-<HHMMSS>.csv
```

Examples:

```
crm-passes-20250801-083000.csv 
loyalty-passes-20250731-235959.csv
```

Notes:

* The file extension must be `.csv`.
* Avoid using special characters (spaces, #, %, &, etc.) in file names
* When a script or integration generates the files, adopting a timestamp-based naming pattern helps avoid duplication and makes debugging easier.
* If the file is large, The Wallet Crew briefly monitors its size to ensure it is fully written before processing.

## Column reference

Only the columns below are read. Any other column is ignored, and the import log lists it so a misspelled column name does not go unnoticed. Column names and prefixes are recognised regardless of case. An empty cell leaves the corresponding value unchanged, except where stated otherwise.

### Pass identifier columns

To select the target pass, use one of the following columns:

* `id`: Internal The Wallet Crew pass identifier
* `id.<externalId>`: External identifier name (as configured on the tenant)\
  Example: `id.y2.customerId`

When `id` has a value, it alone selects the pass. Otherwise, the row targets every pass carrying all the external identifiers given in its `id.<externalId>` columns, which can be several passes. An empty identifier cell is ignored, so a row whose identifier cells are all empty targets no pass and counts as **No match** in the import log.

### Additional data columns

To update additional data on the pass, use the following syntax:

* `additionalData.<key>`: Updates the key in the pass’s `additionalData` dictionary\
  Example: `additionalData.notification_content`

Multiple additionalData keys can be updated in one row. An empty cell removes the key from the pass.

### Pass template

To switch the pass to a different template, include:

* `passType`: The new template name (as defined in The Wallet Crew)

If omitted or empty, the pass will retain its current template. A template that does not exist on the tenant refuses the row.

### Theme

A theme changes the look of a pass without changing its template, for a campaign, a season or a membership tier. To apply or remove one, include:

* `themeId`: The identifier of the theme to apply
* `-` (a single dash) in the `themeId` column: Removes the current theme

If omitted or empty, the pass keeps its current theme. A theme that does not exist on the tenant refuses the row.

### Benefit template

A benefit template describes an advantage, such as a voucher or a welcome gift, that can be granted to passes. To grant one with the update, include:

* `benefitTemplateId`: The identifier of the benefit template

The benefit is granted to each pass the row targets, before the pass is updated. If omitted or empty, no benefit is granted. A benefit template that does not exist on the tenant refuses the row.

### Notification

A notification is sent to the devices holding the pass along with the update. To send one, include:

* `notification`: The default notification text
* `notification.<language>`: The text for one language, using its language code\
  Example: `notification.fr`, `notification.de`

A device whose language matches a language column receives that text; any other device receives the default text. If every notification cell of a row is empty, no notification is sent for that row.

### Metadata refresh

To trigger a recomputation of internal metadata (e.g., barcodes, expiration, display fields):

* `updateMetadata`: Set to `true` to trigger metadata update

### External identifiers

To update an external identifier, use the following syntax:

* `identifiers.<idName>`: Updates the identifier named `idName`\
  Example: `identifiers.y2.customerId`

Every `identifiers.<idName>` column requires a value: a row with an empty one is refused, so that an identifier is never erased by accident.

## Refused rows

Before anything is changed, each row is checked against the tenant's configuration. This prevents a typo in one column from applying half an update, for example a notification announcing a theme that was never applied. A row is refused, and nothing is changed for it, when it:

* names a pass template that does not exist (`Unknown pass template '…'`)
* names a theme that does not exist (`Unknown theme '…'`)
* names a benefit template that does not exist (`Unknown benefit template '…'`)
* has an empty `identifiers.<idName>` cell (`Missing value for identifier '…'`)

A refused row does not stop the file: every other row is still imported. The import log groups refused rows by reason, with the number of rows for each, so a misspelled theme across the whole file shows as one line.

## Delivery pace

Pass updates are not all delivered at once. They are spread at a steady pace over the whole file, so that a large file does not flood the devices, Apple and Google with updates at the same time. The last passes of a large file are therefore updated well after the file has been read. A default pace applies, and an import script can change it per file (see `getThroughput` below).

## Follow an import

Every imported file has an import log in the console, under **Settings → General → SFTP**. It shows what happened to the file without needing access to technical logs.

The import state comes first:

* **Import completed**: every row was read. Some may still have been refused or have failed; the counts below say how many.
* **Import failed**: the import stopped before the last row, with the reason (unreadable file, stopped, or unexpected error). Rows queued before it stopped are still applied.
* **Import interrupted**: the import started but never reported an end, because processing was stopped mid-file. Rows queued before the stop are still applied.
* **Result not recorded yet**: the import is recent and its result is not available yet.

The counts then describe each row read: **Applied** (updates queued for at least one pass), **No match** (no pass carries the given identifiers), **Refused** (see [Refused rows](#refused-rows)) and **Failed** (a technical error). **Pass updates** is the number of passes queued, which can exceed the applied rows when one row targets several passes. Refused rows are listed by reason, and columns that were ignored are listed in a warning.

Finally, each pass touched by the import is listed with its outcome, once its update has run:

* **Updated**: the pass was updated, or its notification was sent.
* **Skipped**: nothing was applied to the pass, typically because another source updated it after the import queued it. The more recent update is kept.
* **Pass not found**: no pass has this identifier, for example a wrong value in the `id` column.
* **Canceled**: the update was canceled before it was applied.
* **Failed**: the update or its notification could not be completed; the error message says which.

### Import events

Each import also emits events that can be sent to an external system through [webhooks](/developers-guides/integration-guides/webhooks.md), for example to alert an operations team when a file fails or when rows are refused, without watching the console. Subscribing to `Import:*` covers all three:

* `Import:Started` when a file starts being read
* `Import:Completed` once every row has been read
* `Import:Failed` when the import stops before its last row

Every event carries an `import` object with `type` (`passes` for this import) and `fileName`. `Import:Completed` and `Import:Failed` add `rows`, with the counts `read`, `applied`, `noMatch`, `refused` and `failed` as in the import log, `passCount`, the number of pass updates queued, and `ignoredColumns`. `Import:Completed` also carries `refusals`, the ten most frequent reasons rows were refused, each with `reason` and `rowCount`. `Import:Failed` carries `reason`: `invalidFile`, `canceled` or `unexpected`.

`Import:Completed` means every row has been read, not that every pass has been updated: the updates are delivered afterwards, at the import's pace.

## SFTP access

Upload the CSV files to the SFTP endpoint for the environment:

* **QA**: `triglav-qa.walletcrew.net` (port `22`)
* **Production**: `triglav.walletcrew.net` (port `22`)

Contact The Wallet Crew Support to request SFTP credentials.

## Examples

### Example 1: Update Notification and Offer URL

```csv
id;additionalData.notification_content;additionalData.offer_url
Gk440DNzvfZcDmlA;Merry Christmas Alice!;https://acme.com/xmas1
HKlhrhrEXx2ZASYs;Merry Christmas Bob!;https://acme.com/xmas1
```

### Example 2: Update by External ID with Loyalty Info

```csv
id.y2.customerId;additionalData.notification_content
00100123;Enjoy 30% off on your next purchase
04503295;Thanks for your purchase! Only 10 points left to redeem a €10 voucher
02319202;Thanks for your purchase! Only 120 points left to redeem a €10 voucher
```

### Example 3: Switch Pass Template and Force Metadata Refresh

```csv
id;passType;updateMetadata
gH67xKlPzLZ99xa2;vip_template;true
dAk21jvUZYx39q77;standard_template;true
```

### Example 4: Update external id y2.customerId and force metadata refresh

```csv
id.y2.customerId;identifiers.y2.customerId;passType;updateMetadata
04503295;1010013295;;true
04503296;1010013296;;true
```

### Example 5: Apply a theme, grant a benefit and notify

The first row moves the member to the Gold theme, grants the welcome benefit and sends a notification, in French on French devices and in English elsewhere. The second row removes the theme of another member and sends nothing.

```csv
id.y2.customerId;themeId;benefitTemplateId;notification;notification.fr
00100123;gold;gold-welcome;Welcome to Gold! A gift is waiting on your card.;Bienvenue en Gold ! Un cadeau vous attend sur votre carte.
04503295;-;;;
```

## Extensibility and custom mapping

The Wallet Crew supports custom file transformations using import scripts.

This makes it possible to:

* Adapt an existing export format (e.g., from a CRM or POS)
* Map legacy column names to The Wallet Crew-compatible keys
* Inject dynamic values (e.g., current date, calculated points)
* Enrich data with lookups from external sources

To customize the import process, create an `import.custom.js` file in the `scripts/` folder in Advanced configuration. The script receives each row and returns what to update; the returned values go through the same checks as the columns above, so an unknown theme returned by a script refuses the row in the same way. A script that throws an error stops the whole file, which shows as **Import failed** in the import log. The full contract is described in [Bulk pass update row transformation](/connectors/custom-connector/custom-connector-scripting-reference/bulk-pass-update-row-transformation.md).

For the following CSV file:

```csv
id.y2.customerId,neo_notification_content,neo_offer_title,neo_offer_body,tier
abc12345,"Hey Arthur !","Your 20% offer","20% Off for your next purchase",gold
```

The following script can be used:

```js
/**
 * Transforms a CSV row into an UpdatePassInformation object.
 *
 * @param {Object<string, string>} row - The CSV row, as a dictionary of column names to values.
 * @returns {Object} Transformed row information.
 * @returns {Object<string, string>} return.Identifiers - Identifiers for this row (e.g., customer ID).
 * @returns {string|null} return.PassType - The pass template to switch to, or null to keep the current one.
 * @returns {string|null} return.ThemeId - The theme to apply, "" to remove the theme, or null to keep it.
 * @returns {string|null} return.BenefitTemplateId - The benefit template to grant, or null to grant none.
 * @returns {string|null} return.Notification - The default notification text, or null to send none.
 * @returns {Object<string, string>} return.NotificationLocalizations - Notification text per language code.
 * @returns {Object<string, string>} return.AdditionalData - Optional additional data from prefixed columns.
 */
function transform(row){
  const identifiers = {
      "id.y2.customerId" : row["id.y2.customerId"]
  };

  const passType = row.passType || null; 

  const properties = [
    "notification_content",
    "offer_title", 
    "offer_body"
   ];

  let additionalData = {};
  for(const property of properties){
    if(row["neo_" + property] !== undefined){
      additionalData[property] = row["neo_" + property].toString(); 
    }
  }

  return {
    Identifiers : identifiers, 
    PassType: passType,
    ThemeId: row.tier ? row.tier : null,
    Notification: row.neo_notification_content || null,
    AdditionalData : additionalData
  }
}

/**
 * Optional method to return a custom throughput for a given import file.
 *
 * @param {string} fileName - The import file name.
 * @returns {number|null} Throughput override, or null to use default.
 */
function getThroughput(fileName) {
  // Example: return null to use default
  return null;
}

export default function(context) {
  context.register('runtime.import.updatePasses.rowTransformer', {
    Transform: transform, 
    GetThroughput: getThroughput
  });
}
```

Contact our team for help implementing custom mapping logic for imports.

## Tips and best practices

* Ensure identifiers are accurate: the **No match** count and the **Pass not found** outcome in the import log show the ones that are not.
* Always test the file format in QA before uploading to production, and check its import log for refused rows and ignored columns.
* Use consistent encoding (UTF-8) to avoid parsing issues with special characters.
* Do not upload the same file twice to retry it: every row is imported again, which sends its notifications a second time. Upload a file with only the rows that need it.

## FAQ

<details>

<summary><strong>Do you create new passes, or only update existing ones?</strong></summary>

This flow updates existing passes. Each row must target a pass using `id` or an external identifier column like `id.y2.customerId`.

</details>

<details>

<summary><strong>What happens if a row has both <code>id</code> and <code>id.&#x3C;externalId></code>?</strong></summary>

The `id` column wins: the row updates the pass with that identifier, and the external identifier columns are not used to find it. Use one identifier per row where possible, which avoids ambiguity and makes troubleshooting easier.

</details>

<details>

<summary><strong>Can I update multiple <code>additionalData</code> keys in one row?</strong></summary>

Yes. Add one column per key, using `additionalData.<key>`. The row will update all provided keys in one pass update.

</details>

<details>

<summary><strong>Why was a row refused?</strong></summary>

The row names a pass template, theme or benefit template that does not exist on the tenant, or leaves an `identifiers.<idName>` cell empty. The import log lists each reason with the value at fault, for example `Unknown theme 'gold-2026'`. Fix the value, then upload a file with only the refused rows.

</details>

<details>

<summary><strong>How do I remove a theme from a pass?</strong></summary>

Set the `themeId` cell to a single dash, `-`. An empty cell leaves the current theme in place.

</details>

<details>

<summary><strong>A pass shows as Skipped in the import log. Why?</strong></summary>

The pass was usually updated by another source, such as the API or another import, after this import queued its update. To avoid pushing the same pass to its devices twice, the more recent update is kept and the import's changes are not applied to that pass. Import the row again if its changes are still needed.

</details>

<details>

<summary><strong>My file has a column the import log reports as ignored. What does it mean?</strong></summary>

The column name is not one of those in the column reference, often because of a typo such as `additionaldata-balance` or `theme`. Its values were not applied. Rename the column and upload the affected rows again.

</details>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.thewalletcrew.io/configure/advanced-configuration/wallet/import-and-export/update-pass-using-flat-files.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
