> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.dropboxapi.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.dropboxapi.com/_mcp/server.

# Writing a Script

**Note:** This tutorial was written based on an old version of our documentation. The concepts are the same, but the screenshots and references are outdated. Apologies for the inconvenience while we work to update these resources.

## Building a simple expense organizer app

In the previous sections, we explored creating an app and navigating the App Console, we then took a look at the API documentation and learned how to quickly test endpoints. Now we're ready to put all of that knowledge into action and build our expense organizer app.

## App Design

First, let's define what we want our app to do. Given a folder with several unorganized expense files (invoices, receipts, itineraries, etc.), we're going to write a simple tool to sort them into sub-folders based on their modification time.

The sub-folders will be organized by year, and then by month. To do this we'll need to:

1. List all of the files in the source folder
2. Determine the modification time of the files
3. Build a folder structure for each year and month of the modification times found on the files
4. Move the files to their proper sub-folders

We now need to find API endpoints that match up to these actions. At the end of the last section we tested the list\_folder and list\_folder\_continue endpoints in the API Explorer. As a reminder, the response from those endpoints looks like this:

![](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/dropbox-api.docs.buildwithfern.com/c015337e96618744adc4920f1aaa7b2b219aa3e210fdb967dcd57ffc1383657c/docs/assets/migrated/getting-started/9f86702bce.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260912%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260912T072630Z&X-Amz-Expires=604800&X-Amz-Signature=f41c6cb0f02d72ea9fe41e78bcd39edd83fb80f408d2d53275d18bba15df96af&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject)

If we examine this we can see that, for `FileMetadata`, the modification times are already included in the results. This means we can solve steps 1 and 2 from above with this single endpoint call.

Next up is step 3, building a folder structure. Looking through the API documentation [create\_folder\_v2](/dropbox-api/api-reference/user-endpoints/files/create-folder-v-2) looks like just the thing to create those folders.

Last up, step 4, is moving the files, and again a quick scan of the documentation surfaces the [move\_v2](/dropbox-api/api-reference/user-endpoints/files/move-v-2) endpoint which is exactly what we need.

By reading the documentation and testing endpoints in the API Explorer, we now know which endpoints we need to use to write our script:

* list\_folder
* list\_folder\_continue
* create\_folder\_v2
* move\_v2

We know what we need to do, and we know how to do it. Now it's time to start writing some code.

## Building the app

For this guide we'll be writing the organizer script in Python and we're going to leverage the Dropbox Python SDK to make accessing endpoints even easier.

Before we start writing code, be sure to install python & the Dropbox SDK as described in the [overview](/dropbox-api/docs/get-started/tutorial/overview).

Once we've installed the SDK, we can now import the SDK into our program with:

```python
import dropbox
```

The next step is to create a Dropbox client instance with an API access token. If you haven't already generated an access token for your app please refer to the "Creating a Dropbox App and Navigating the App Console" section of this guide for information on how you can generate a token from the App Console. Once you have a token, create your Dropbox API instance:

```python
import dropbox

print("Initializing Dropbox API...")
dbx = dropbox.Dropbox("<ACCESS TOKEN>")
```

Next, referring to our app design, we need to get a list of all the files to be processed. Since we're using the App folder permission model, we can only see files placed into our app root folder (/Apps/\<App Name>/) and below. So let's consider our app root folder as the staging area for files to be sorted, and we'll make all of our year and month-based sub-solders directly beneath that. If you recall from earlier in the guide, when using App folder permissions while your app folder resides at /Apps/\<App Name> that path is presented to your App as root or "/" so in order to list files in our app root folder we will pass an empty path string parameter to the [list\_folder](/dropbox-api/api-reference/user-endpoints/files/list-folder) endpoint, if we were to pass "/Apps/\<App Name>" we would receive a path not found error since effectively we'd be asking for a listing of files and folders at "/Apps/\<App Name>/Apps/" which (unless you actively have gone and created it) does indeed not exist.

Let's add a call to list\_folder:

```python
print("Scanning for expense files...")
result = dbx.files_list_folder(path="")
```

Here we create a list to hold all of the file and folder entries and then make a call to the `files_list_folder()` method of the SDK. Let's look at that call for a moment:

```python
result = dbx.files_list_folder(path="")
```

In this call, we set the path parameter to an empty string which represents the root of our app folder. This is exactly what we want. As an exercise for the reader, it may be worth taking a moment to look over some of the other parameters that could be set here such as 'limit' or 'recursive' and considering how changing their defaults would affect the results of this call. However, for the moment, let's leave all other parameters at their default.

The [files\_list\_folder()](https://dropbox-sdk-python.readthedocs.io/en/latest/api/dropbox.html#dropbox.dropbox.Dropbox.files_list_folder) method returns a [ListFolderResult](https://dropbox-sdk-python.readthedocs.io/en/latest/api/files.html#dropbox.files.ListFolderResult) object (described in the 'Returns' section of the documentation) which is stored in a placeholder variable. Processing this result is a little more complex than it seems. Looking at the description of the `files_list_folder()` method we will see some guidance around properly processing the various types of entries that we might get back. It's highly recommended to read the documentation in full, but we'll summarize some of the important points here.

When listing the contents of a folder, we have to remember that the file system is "live" - content can be changed in the period between our initial call to `files_list_folder()` and any subsequent calls to `files_list_folder_continue()` (if we are required to make them). Because of this, we need to examine the entries returned to us and update the current state of our "view" of the file system, until we receive a response with `has_more` set to `False`. To represent our view of the file system we'll use a Python dictionary called `files` and in that we'll store entities returned from the `files_list_folder()` and `files_list_folder_continue()` endpoints. But only after we do a little more processing. Let's look at making a function to handle this processing:

```python
def process_folder_entries(current_state, entries):
    for entry in entries:
        if isinstance(entry, dropbox.files.FileMetadata):
            current_state[entry.path_lower] = entry
        elif isinstance(entry, dropbox.files.DeletedMetadata):
            current_state.pop(entry.path_lower, None) # ignore KeyError if missing
    return current_state
```

This function will take two arguments - the current state of our filesystem view, and a list of new entries to process into it. When we call this function we'll pass our `files` dictionary into it for the current state, and the list of entries from the result of each API endpoint call as the new entries to be processed.

The `for` loop begins the work of examining each entry, determining its instance type, and then updating the `current_state` variable based on the instance type and the actions described for it in the `files_list_folder()` documentation. Let's dive into what happens inside the `for` loop a bit more before moving on.

From the documentation we know that `files_list_folder()` will return a `ListFolderResult` object. Within that object, the `entries` attribute contains a list of items representing one of these metadata instances:

* `FileMetadata` - Represents a file in the filesystem
* `FolderMetadata` - Represents a folder in the filesystem
* `DeletedMetadata` - Indicates that there used to be a file or folder at this path, but it no longer exists

In general, each of these metadata instances should be checked for, and the current state should be updated accordingly. But in this case, since we know we that we don't need to process folders for our app, we can ignore `FolderMetadata` checks for the moment. Instead we'll focus on `FileMetadata` and `DeletedMetadata` to update our current state. The `if/elif` statement checks for the instance type of the current entry and then updates state accordingly, for `FileMetadata` this means adding them to the dictionary, for `DeletedMetadata` we'll remove them from the dictionary. Note that while we mentioned earlier that `DeletedMetadata` might contain metadata for files or folders, we're not checking for that here. This won't cause issues in this specific case since if we encounter a `FolderMetadata` instance and try to remove it, we can silently ignore the error that will be thrown. However, generally, you will want to check for, and handle, each metadata type appropriately.

So with the new function in place, let's redo our call to `files_list_folder` and properly process the results:

```python
print("Scanning for expense files...")
result = dbx.files_list_folder(path="")
files = process_folder_entries({}, result.entries)
```

Here, we pass the list of new metadata entries (contained in `result.entries`) to our `process_folder_entries()` function. We return an updated dictionary from that function that we store in `files` to represent our current state.

Now we can just loop through the files in `files` and start moving files, right? Well, not just yet. Remember that the `ListFolderResult` returned from `files_list_folder()` and `files_list_folder_continue()` contains a very important property `has_more` which indicates if there are additional files or folders that were not returned in the results of our most recent function call. If `has_more` is `True` it indicates we need to make additional calls to [list\_folder\_continue()](https://dropbox-sdk-python.readthedocs.io/en/latest/api/dropbox.html#dropbox.dropbox.Dropbox.files_list_folder_continue) in order to retrieve all of the content we're looking for. In order to make the call to `list_folder_continue()`, we also need another property of `ListFolderResult` which is `cursor`. The cursor represents a placeholder for our current position in the list of content and tells `list_folder_continue()` where to pick up from on subsequent calls. Since we don't know how many times we might need to call `list_folder_continue()` to retrieve the full listing of content, it's probably best to call it from a loop and use the state of `has_more` as our loop termination condition. This might look something like this:

```python
print("Scanning for expense files...")
result = dbx.files_list_folder(path="")
files = process_folder_entries({}, result.entries)

# check for and collect any additional entries
while result.has_more:
    print("Collecting additional files...")
    result = dbx.files_list_folder_continue(result.cursor)
    files = process_folder_entries(files, result.entries)
```

Here, we add a while loop that will test for the status of `has_more` and as long as it's `True`, we will keep calling `list_folder_continue()` with the updated `cursor` from the result and process the new entries to update the state of `files`.

At this point we should have a complete listing of all the content in the root of our app folder stored in `files`, now we can loop through the entries and start doing work.

We know that the `FileMetadata` instance will contain the modified time so we can use that to construct the destination path for our expense files that we'll pass to `files_create_folder()`. One thing to note is that `files_create_folder()`, if called with an existing path, will throw an exception. So to avoid creating paths more than once and triggering this, we can add in a simple check to see if a given path exists:

```python
from dropbox.exceptions import ApiError

# ...

def path_exists(path):
    try:
        dbx.files_get_metadata(path)
        return True
    except ApiError as e:
        if e.error.get_path().is_not_found():
            return False
        raise
```

This simple function takes a path parameter and tries to retrieve metadata for that path. In the event that we get a valid result back, we simply return `True` as this implies the path does exist. Should we get any exceptions, we check for a `not_found` error and return `False` indicating the path does not exist. All other exceptions we re-raise so they can be properly handled in the calling code as appropriate.

Using this new function we can create our destination path and add our call to `files_create_folder()`:

```python
import posixpath

# ...

for entry in files.values():
    # use modified time of file to build destination path
    destination_path = posixpath.join(
        "/" + str(entry.client_modified.year) + "_Expenses",
        str(entry.client_modified.month)
    )
    # check to see if we need to create the destination folder
    if not path_exists(destination_path):
        print("Creating folder: {}".format(destination_path))
        dbx.files_create_folder(destination_path)
```

Here, we build the destination path which will look like:

```text
/<YEAR>_Expenses/<MONTH>
```

We use the client modified time to build our path here, but there is also a server modified time that can be used. Just be aware that the client modified time can be set by any app via the upload endpoints and hence it might be less reliable than the server modified values.

Ok, if needed, we have created the required path to sort our file into. Let's now perform the move. We can do this by adding a call to `files_move_v2()` using the existing file path stored in `entry.path_lower` and the newly constructed `destination_path`:

```python
for entry in files.values():
    # ...
    print("Moving {} to {}".format(entry.path_display, destination_path))
    dbx.files_move_v2(entry.path_lower, destination_path)
```

And with a few final touches, our first app should be complete:

```python
import dropbox
import posixpath
from dropbox.exceptions import ApiError

def process_folder_entries(current_state, entries):
    for entry in entries:
        if isinstance(entry, dropbox.files.FileMetadata):
            current_state[entry.path_lower] = entry
        elif isinstance(entry, dropbox.files.DeletedMetadata):
            current_state.pop(entry.path_lower, None) # ignore KeyError if missing
    return current_state

def path_exists(path):
    try:
        dbx.files_get_metadata(path)
        return True
    except ApiError as e:
        if e.error.get_path().is_not_found():
            return False
        raise

print("Initializing Dropbox API...")
dbx = dropbox.Dropbox("<ACCESS TOKEN>")

print("Scanning for expense files...")
result = dbx.files_list_folder(path="")
files = process_folder_entries({}, result.entries)

# check for and collect any additional entries
while result.has_more:
    print("Collecting additional files...")
    result = dbx.files_list_folder_continue(result.cursor)
    files = process_folder_entries(files, result.entries)

for entry in files.values():
    # use modified time of file to build destination path
    destination_path = posixpath.join(
        "/" + str(entry.client_modified.year) + "_Expenses",
        str(entry.client_modified.month)
    )

    # check to see if we need to create the destination folder
    if not path_exists(destination_path):
        print("Creating folder: {}".format(destination_path))
        dbx.files_create_folder(destination_path)

    print("Moving {} to {}".format(entry.path_display, destination_path))
    dbx.files_move_v2(entry.path_lower, destination_path + "/" + entry.name)
print("Complete!")
```

Let's test it out.

We've seeded our app folder with some sample files that have a range of modification times:

![](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/dropbox-api.docs.buildwithfern.com/fac8c40f3f11d19c51ed06954cf349ef887ce9b3e6710ffff9b575429e858c93/docs/assets/migrated/getting-started/171b86ea4d.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260912%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260912T072630Z&X-Amz-Expires=604800&X-Amz-Signature=846dc72eaea5128ed48a5851560d11f567852e2502e59e5d2450859924e2cb12&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject)

We can see there's a mix of 2017 and 2018 dates so we should get at least two folders created for years and then some number of subfolders under each of them for each month represented in the sample files. Let's run the app and see the results:

![](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/dropbox-api.docs.buildwithfern.com/9cea23779e91b05b54514f5119b0848dd4efc63db8ad4587445636cda3d878e3/docs/assets/migrated/getting-started/e2032b993b.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260912%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260912T072630Z&X-Amz-Expires=604800&X-Amz-Signature=a57ef7e3032f178d0ca3b68adaa846bbce3f9eca5d6079fd5d2ff1464c3b8ded&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject)

And now our app folder contains:

![](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/dropbox-api.docs.buildwithfern.com/0b697e581943fc84963aec8d351b0d8b717a11a68ab8214a168ec9422961d732/docs/assets/migrated/getting-started/cbd85ed61e.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260912%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260912T072630Z&X-Amz-Expires=604800&X-Amz-Signature=145e4dea5bb13a0a047e3e8a95a0c184ab4b4ba39f6666a4897164920a63a8ca&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject)

Diving into one of those folders shows us this:

![](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/dropbox-api.docs.buildwithfern.com/da6a3eab429b9c9d88b3f637360d410e2af2994b022a6d7b1ad5d281dd3460e7/docs/assets/migrated/getting-started/0fd99d5291.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260912%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260912T072630Z&X-Amz-Expires=604800&X-Amz-Signature=a180b0fb15c0418da1c82e6252f5c1293f9a0223b75f434e820b5bd540634cf8&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject)

And one more step down, into a given month, shows our files nicely sorted:

![](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/dropbox-api.docs.buildwithfern.com/1b2261c12e96a6d46cd260c471896dbe92e206b65cdd296377c4ac684a13ee03/docs/assets/migrated/getting-started/76c31abe55.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260912%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260912T072630Z&X-Amz-Expires=604800&X-Amz-Signature=218e1350dc9b1a7b3d0914930465fd9b4d8945a7a44bbdee892b81068755f9b8&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject)

All we need to do going forward is drop new expense files into our app folder over time and then run our app to sort them.

## Summary

That's it! We did it! Our first app on the DBX Platform is up and running, but development certainly doesn't stop here. While we now have a good introduction to developing on the DBX Platform, and we've built a useful tool, there are still several features we're missing. Right now, only we can use our app. If we want others to be able to use it we'll need to add an [OAuth 2 authorization flow](/dropbox-api/docs/oauth). Also, currently, our app must be run manually each time we want to organize our files and while we could add it to cron or task scheduler to run at set intervals, there is a better way to manage this: [webhooks](/dropbox-api/docs/webhooks). In future articles we'll cover how to extend and enhance our app to support these features, but in the meantime here are a few next steps you can explore to continue building with the DBX Platform:

* Add the capability to append a timestamp to the file name when the file is moved.
* Limit the app to only organize certain file types/extensions (PDF, JPG, DOCX, etc.)
* Include MediaMetadata (hint: you'll need to change your call to files\_list\_folder()) and sort images based on location data vs timestamps
* Add the capability to persist the final cursor returned from files\_list\_folder\_continue(), add/remove/update content in your app folder and see what subsequent calls to files\_list\_folder\_continue() returns. How can this be used to improve the app?
* Build a companion app to upload/download expense files to your app folder via the API, or add this functionality to an existing app by integrating the Dropbox [Chooser](/dropbox-api/docs/pre-built-components/chooser) or [Saver](/dropbox-api/docs/pre-built-components/saver) into it
* Incorporate an [OAuth authorization flow](/dropbox-api/docs/oauth)
* Deploy your app to a cloud infrastructure service such as AWS, Heroku, or Google App Engine
* Review the [Branding Guide](/dropbox-api/docs/developer-resources/branding-guide) and make sure your app is ready for the [production approval](/dropbox-api/docs/developer-resources/developer-guide#production-approval) process

When you're ready to explore other DBX Platform examples you can start with the [Node Photo Gallery](https://dropbox.github.io/nodegallerytutorial/).