# Payhawk → Google Drive receipt sync (`apify/payhawk-google-drive-receipt-sync`) Actor

Automatically archive Payhawk expense receipts (PDFs) to a Google Drive folder. Incremental, idempotent, schedule-friendly — every new receipt lands in Drive without duplicates.

- **URL**: https://apify.com/apify/payhawk-google-drive-receipt-sync.md
- **Developed by:** [Apify](https://apify.com/apify) (Apify)
- **Categories:** Automation
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, NaN bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-usage

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

### What is the Payhawk → Google Drive receipt sync?

This Actor automatically copies every **expense receipt PDF** from your [Payhawk](https://payhawk.com/) account into a **Google Drive folder**. You set it up once, run it on a schedule, and every new receipt your team uploads to Payhawk lands in Drive — ready for your accountant, your bookkeeping system, or long-term archival. No more downloading attachments one expense at a time.

The Actor uses Payhawk's official Developer API to list expenses that have not yet been exported, downloads each receipt, uploads it to a Google Drive folder of your choice, and tells Payhawk that the receipt has been exported — all without duplicates.

### What can this Payhawk to Google Drive Actor do?

- **Incremental sync.** Only processes expenses Payhawk reports as *not exported*, so each run is fast and skips work already done.
- **Idempotent.** Files are named deterministically (`payhawk-<expenseId>.pdf`). If a file with the same name already exists in the destination folder, the Actor reuses it instead of creating a duplicate.
- **Multi-file expenses.** Expenses with several attached receipt pages are uploaded as `payhawk-<expenseId>-1.pdf`, `payhawk-<expenseId>-2.pdf`, and so on.
- **Marks expenses as exported in Payhawk.** Writes an entry to the Payhawk export history so each expense disappears from the *not exported* list and your team can see where the receipt was sent.
- **Dry-run mode.** Validate everything is wired up correctly before any file is written to Drive.
- **Detailed run report.** Every processed expense becomes one row in the dataset (status, file name, Drive link, error if any) for run-by-run auditing.
- **Powered by the Apify platform.** Schedule runs, monitor failures, integrate with your other tools, and access results via the [Apify API](https://docs.apify.com/api/v2) — all out of the box.

### How does the Payhawk to Google Drive sync work?

On each run, the Actor:

1. Calls Payhawk `GET /accounts/:accountId/expenses` with the filter `export.status = not-exported`, paginating with `$skip`/`$take`.
2. For each expense, reads `expense.document.files[]` and downloads each attached receipt PDF.
3. Uploads each PDF to your Google Drive folder. The folder must live in a **Shared Drive** so the Google service account can write to it.
4. Calls Payhawk `POST /expenses/:expenseId/export-history` with `exportStatus: succeeded` and a link to the Drive file, marking the expense exported.
5. Records the synced expense ID in a named key-value store as a safety net against duplicates across runs.

### How much will it cost to sync Payhawk receipts to Google Drive?

The Actor is billed in [Apify Compute Units (CUs)](https://docs.apify.com/platform/actors/running/compute-units). It does no heavy CPU work — every run is mostly waiting on Payhawk and Google Drive responses — so usage is low even with hundreds of receipts. As a rough order of magnitude, a few hundred receipts per run on default memory settings will consume a small fraction of a CU. The free tier on Apify is plenty for evaluating the Actor on a typical month of expenses.

The Payhawk and Google Drive APIs the Actor calls are free to use within their published rate limits (Payhawk's developer API is rate-limited to 15 requests per second). The sync is well under that ceiling.

### How to set up the Payhawk to Google Drive Actor

#### Step 1: Get your Payhawk API key

In Payhawk, go to **Settings → Integrations → API → Create key**. Copy the key (it is sent in the `X-Payhawk-ApiKey` header). The account ID is shown alongside the key and also appears in any Payhawk URL.

Put the values into `payhawkApiKey` and `payhawkAccountId`.

#### Step 2: Create a Google service account

Service accounts are how a long-running Actor can write to Drive without an interactive Google login.

1. In the [Google Cloud Console](https://console.cloud.google.com/): **IAM & Admin → Service Accounts → Create service account**. Any name works (e.g. `payhawk-sync`).
2. Open the new service account → **Keys → Add key → Create new key → JSON**. Save the downloaded JSON; you will paste it into `googleServiceAccount`.
3. In **APIs & Services → Library**, search for and **enable the Google Drive API** for the project.

#### Step 3: Create a Shared Drive folder and share it with the service account

**This step is required.** Service accounts have no storage quota in a personal *My Drive*. Uploads to a regular My Drive folder fail with `Service Accounts do not have storage quota`. A Shared Drive is the supported way to give a service account a place to write.

1. In [Google Drive](https://drive.google.com/), open the left sidebar → **Shared drives → + New**. Name the drive anything you like (for example, `Payhawk receipts`).
2. Inside the Shared Drive, create a folder for the receipts.
3. Open the Shared Drive → **Manage members** → add your service account's email (`xxx@<project>.iam.gserviceaccount.com`, found in the JSON you downloaded) with the **Content manager** role.
4. Open the destination folder and copy the ID from the URL: `drive.google.com/drive/folders/<id>`. Paste it into `driveFolderId`.

#### Step 4: Run with dry-run enabled

For the first run, set `dryRun` to `true`. The Actor will list expenses and download receipts but will not write to Drive or Payhawk. Confirm the logs look right (e.g. the expected number of expenses are found), then flip `dryRun` to `false` and run for real.

#### Step 5: Schedule it

Once a real run succeeds, create an Apify schedule at the cadence you want (daily and hourly are common choices).

### Input

The Actor takes the following inputs — see the **Input** tab for the full schema with descriptions and secret handling.

- `payhawkApiKey` — Payhawk API key.
- `payhawkAccountId` — Payhawk account ID.
- `googleServiceAccount` — Service account JSON for Google Drive access.
- `driveFolderId` — Destination Drive folder ID (must live inside a Shared Drive).
- `exportedInto` (optional) — Label written to Payhawk's export history. Defaults to `Google Drive`.
- `dryRun` (optional) — When `true`, skip all writes. Defaults to `false`.

### Output

The Actor writes one dataset row per processed expense, with the upload status and a link to the file in Drive. You can download the dataset in JSON, CSV, Excel, HTML, RSS, or XML — same as any Apify Actor.

```json
[
  {
    "expenseId": "224",
    "status": "uploaded",
    "fileName": "payhawk-224.pdf",
    "driveFileId": "1a2BcDeFgHIjkLmnOPqRsTuvWxYz",
    "driveFileUrl": "https://drive.google.com/file/d/1a2BcDeFgHIjkLmnOPqRsTuvWxYz/view"
  },
  {
    "expenseId": "225",
    "status": "reused",
    "fileName": "payhawk-225.pdf",
    "driveFileId": "9Z8YxWvUtSrQpOnMlKjIhGfEdCbA",
    "driveFileUrl": "https://drive.google.com/file/d/9Z8YxWvUtSrQpOnMlKjIhGfEdCbA/view"
  },
  {
    "expenseId": "226",
    "status": "no-receipt"
  }
]
````

Status values: `uploaded`, `reused` (file already existed in Drive), `skipped-already-synced` (already in local dedupe state), `no-receipt` (expense had no attached document), `error`.

### FAQ

#### Why does the upload fail with "Service Accounts do not have storage quota"?

The destination folder is in a personal *My Drive* instead of a Shared Drive. Service accounts can be granted access to a My Drive folder, but they cannot own files there, and Drive will refuse the upload. Move the folder into a Shared Drive (see Step 3 of Setup) and use that folder ID instead.

#### Can I sync to a personal My Drive folder?

Not with a service account. The alternative is **OAuth domain-wide delegation**, which makes the service account impersonate a Workspace user, so uploaded files use that user's quota. This requires a Google Workspace administrator to grant the delegation. Most teams find creating a Shared Drive easier.

#### Will running the Actor twice create duplicate files in Drive?

No. The Actor checks for an existing file with the same `payhawk-<expenseId>.pdf` name in the destination folder before uploading, and Payhawk's `export.status` filter excludes already-exported expenses. As an extra safety net, synced expense IDs are tracked in a named key-value store.

#### What happens to expenses without an attached receipt?

They are reported as `no-receipt` in the dataset and skipped. The expense is not marked exported in Payhawk, so it will be re-evaluated on the next run (giving your team a chance to upload the receipt).

#### Is this a two-way sync?

No, it's one-way: Payhawk → Google Drive. Files deleted from Drive are not re-uploaded on the next run, because the expense has already been marked exported in Payhawk.

#### How do I change the label that appears in Payhawk's export history?

Set the `exportedInto` input. It defaults to `Google Drive`. During testing, change it to something like `Test sync` so test runs are clearly distinguishable in Payhawk's UI from real ones.

#### How do I run the Actor locally?

Clone the repo, run `npm install`, put your inputs in `storage/key_value_stores/default/INPUT.json`, then run `npx apify run`. Use `dryRun: true` to validate the setup without touching Drive or Payhawk.

# Actor input Schema

## `payhawkApiKey` (type: `string`):

API key used to authenticate against the Payhawk API. Create it in Payhawk under Settings → Integrations → API.

## `payhawkAccountId` (type: `string`):

Your Payhawk account ID — the `accountId` path segment shown alongside the API key.

## `googleServiceAccount` (type: `object`):

Service account credentials JSON. The service account email must be added as a Content manager of the target Shared Drive (service accounts cannot upload to a personal My Drive).

## `driveFolderId` (type: `string`):

ID of the destination folder inside a Shared Drive. It's the trailing segment of the folder URL: drive.google.com/drive/folders/<id>.

## `exportedInto` (type: `string`):

Value written to Payhawk's export history `exportedInto` field — visible in the Payhawk UI to indicate where the receipt was sent. Override for testing (e.g. 'Test sync') before switching to your production label.

## `dryRun` (type: `boolean`):

When enabled, the Actor lists expenses and downloads receipts but does NOT upload to Drive or write back to Payhawk. Use for the first test run.

## Actor input object example

```json
{
  "exportedInto": "Google Drive",
  "dryRun": false
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("apify/payhawk-google-drive-receipt-sync").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("apify/payhawk-google-drive-receipt-sync").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call apify/payhawk-google-drive-receipt-sync --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=apify/payhawk-google-drive-receipt-sync",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Payhawk → Google Drive receipt sync",
        "description": "Automatically archive Payhawk expense receipts (PDFs) to a Google Drive folder. Incremental, idempotent, schedule-friendly — every new receipt lands in Drive without duplicates.",
        "version": "0.0",
        "x-build-id": "oCF0dXDGhX7IsNxlW"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/apify~payhawk-google-drive-receipt-sync/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-apify-payhawk-google-drive-receipt-sync",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/apify~payhawk-google-drive-receipt-sync/runs": {
            "post": {
                "operationId": "runs-sync-apify-payhawk-google-drive-receipt-sync",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/apify~payhawk-google-drive-receipt-sync/run-sync": {
            "post": {
                "operationId": "run-sync-apify-payhawk-google-drive-receipt-sync",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "payhawkApiKey",
                    "payhawkAccountId",
                    "googleServiceAccount",
                    "driveFolderId"
                ],
                "properties": {
                    "payhawkApiKey": {
                        "title": "Payhawk API key",
                        "type": "string",
                        "description": "API key used to authenticate against the Payhawk API. Create it in Payhawk under Settings → Integrations → API."
                    },
                    "payhawkAccountId": {
                        "title": "Payhawk account ID",
                        "type": "string",
                        "description": "Your Payhawk account ID — the `accountId` path segment shown alongside the API key."
                    },
                    "googleServiceAccount": {
                        "title": "Google service account JSON",
                        "type": "object",
                        "description": "Service account credentials JSON. The service account email must be added as a Content manager of the target Shared Drive (service accounts cannot upload to a personal My Drive)."
                    },
                    "driveFolderId": {
                        "title": "Google Drive folder ID",
                        "type": "string",
                        "description": "ID of the destination folder inside a Shared Drive. It's the trailing segment of the folder URL: drive.google.com/drive/folders/<id>."
                    },
                    "exportedInto": {
                        "title": "Exported-into label",
                        "type": "string",
                        "description": "Value written to Payhawk's export history `exportedInto` field — visible in the Payhawk UI to indicate where the receipt was sent. Override for testing (e.g. 'Test sync') before switching to your production label.",
                        "default": "Google Drive"
                    },
                    "dryRun": {
                        "title": "Dry run",
                        "type": "boolean",
                        "description": "When enabled, the Actor lists expenses and downloads receipts but does NOT upload to Drive or write back to Payhawk. Use for the first test run.",
                        "default": false
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
