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

# Collect expense receipts with webhooks

> Download receipts and supporting documents the moment they are uploaded to an expense

Receipts for card and UPI expenses are usually uploaded **after** the payment. By then the transaction event has already been sent, so waiting for it won't get you the receipt. The `expense.attachment.updated` event fires each time a document is added or removed, with a key you can download right away.

## Before you start

* [Set up a webhook endpoint](/webhooks/setup) subscribed to `expense.attachment.updated`.
* Create an API key, as described in the [quickstart](/#quickstart). Your handler uses it to download files.

## Walkthrough

<Steps>
  <Step title="Receive the event">
    An employee uploads a receipt. Pazy sends `expense.attachment.updated` with `action: "ATTACHMENT_ADDED"` and `attachmentType: "RECEIPT"`.
  </Step>

  <Step title="Choose the download endpoint">
    Use `attachmentType` to pick the path:

    * `RECEIPT` uses `/download/expense`
    * `SUPPORTING_DOCUMENT` uses `/download/expense-attachment`
  </Step>

  <Step title="Download the file">
    Call the [File Download API](/apis/download-file) with `fileKey` set to `data.documentKey`.
  </Step>

  <Step title="Store it against the expense">
    Save the file with the expense `id` from the event. Use `name` and `mimeType` from the event for the file name and type.
  </Step>
</Steps>

## Example handler

```javascript theme={null}
const DOWNLOAD_PATH = {
  RECEIPT: 'expense',
  SUPPORTING_DOCUMENT: 'expense-attachment',
};

async function handleAttachment(data) {
  if (data.action === 'ATTACHMENT_DELETED') {
    // Remove it from your own store. The key no longer resolves in Pazy
    return storage.remove(data.id, data.documentKey);
  }

  const path = DOWNLOAD_PATH[data.attachmentType];
  if (!path) return; // an attachment type we don't know yet

  const url = new URL(`https://api.pazy.io/v1.0/download/${path}`);
  url.searchParams.set('fileKey', data.documentKey);

  const response = await fetch(url, {
    headers: { Authorization: `Api-Key ${process.env.PAZY_API_KEY}` },
  });
  if (!response.ok) {
    // Throw so your job queue retries. Don't swallow download errors
    throw new Error(`Download failed with ${response.status}`);
  }

  const file = Buffer.from(await response.arrayBuffer());
  await storage.save(data.id, data.documentKey, file, {
    name: data.name,
    mimeType: data.mimeType,
  });
}
```

## Things to get right

* **Download straight away.** A `documentKey` stops resolving, with a `404`, once the document is removed or replaced.
* **Expect replacements.** An expense has one receipt. Replacing it sends `ATTACHMENT_DELETED` for the old file, then `ATTACHMENT_ADDED` for the new one. Process both, and keep the latest.
* **Handle deletes.** If you mirror files, delete your copy on `ATTACHMENT_DELETED`.
* **Do the download in a background job.** Acknowledge the webhook first, as in [Delivery and retries](/webhooks/delivery-and-retries#respond-fast-process-later), so a slow download doesn't cause a timeout and a retry.
* **Links raise no event.** Link-type attachments have no file, so you won't be notified about them.
* **Dedupe.** Key your storage on the expense `id` and `documentKey`, so a duplicate delivery doesn't create a second copy.


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