DEV Community

linou518
linou518

Posted on

How I Recovered 73 Accidentally Deleted Receipts Using the freee API

How I Recovered 73 Accidentally Deleted Receipts Using the freee API

A real incident log. I managed to recover everything within an hour, so I'm writing this down.

What Happened

While organizing the development directory for the techsfree-fr agent (/mnt/shared/agents/techsfree-fr/), February's receipt files disappeared.

04_Finance/2025-2026/02/
  ├── *.jpg  (receipt photos)
  └── *.pdf  (invoice PDFs)
Enter fullscreen mode Exit fullscreen mode

The likely cause: a cleanup script moved them to .tmp/ and deleted them. The archive only had files up to February 17th, leaving 73 files missing.

Why Recovery Was Possible

There was a file called .freee_uploaded.json inside 04_Finance/.

[
  {
    "filename": "2026_02_01.jpg",
    "receipt_id": 123456,
    "uploaded_at": "2026-02-01T10:23:00+09:00"
  },
  ...
]
Enter fullscreen mode Exit fullscreen mode

Every file uploaded to freee was recorded here with its receipt_id. This was the lifeline.

Using the freee Receipt Download API

freee provides a receipt download API.

GET /api/1/receipts/{receipt_id}/download?company_id={company_id}
Authorization: Bearer {access_token}
Enter fullscreen mode Exit fullscreen mode

The response is the raw binary of the image or PDF. Check Content-Type and Content-Disposition to determine the filename.

The Access Token Had Expired

Looking at .freee_token.json, the expires_at was in the past. However, the refresh_token was still valid and could be used to get a new one.

curl -X POST https://accounts.secure.freee.co.jp/public_api/token \
  -d "grant_type=refresh_token" \
  -d "client_id=${CLIENT_ID}" \
  -d "client_secret=${CLIENT_SECRET}" \
  -d "refresh_token=${REFRESH_TOKEN}"
Enter fullscreen mode Exit fullscreen mode

Retrieved a new access_token and updated .freee_token.json.

Batch Recovery Script (Overview)

Just load .freee_uploaded.json and download each file one by one.

import json, requests, os

with open('.freee_uploaded.json') as f:
    records = json.load(f)

headers = {"Authorization": f"Bearer {access_token}"}
company_id = 12345678

for r in records:
    receipt_id = r["receipt_id"]
    filename = r["filename"]

    # Skip existing files
    if os.path.exists(f"02/{filename}"):
        print(f"SKIP: {filename}")
        continue

    resp = requests.get(
        f"https://api.freee.co.jp/api/1/receipts/{receipt_id}/download",
        params={"company_id": company_id},
        headers=headers
    )

    if resp.status_code == 200:
        with open(f"02/{filename}", "wb") as out:
            out.write(resp.content)
        print(f"OK: {filename}")
    else:
        print(f"FAIL: {filename} ({resp.status_code})")
Enter fullscreen mode Exit fullscreen mode

Result:

  • JPG: 65 files ✅
  • PDF: 8 files ✅
  • Skipped (already existed): 4
  • Failed: 0

Key Takeaways

Never delete or move .freee_uploaded.json.

This file records the receipt_id for everything uploaded to freee. As long as this file exists, you can recover local files via the API even if they're deleted. Put another way: as long as the data exists on the SaaS side, you can retrieve it via API as long as you have the IDs.

One more thing: the refresh_token has a longer expiry than the access_token. Don't panic if the access token has expired — just use the refresh token. In freee's case, refresh tokens are valid for 30 days by default.

Summary

Situation Solution
Local files deleted Recover via receipt_id in .freee_uploaded.json
access_token expired Reissue using refresh_token
Batch recovery of many files GET /api/1/receipts/{id}/download with a loop

If you build SaaS integration tools yourself, treat the upload ID record file as an "off-site backup index." The files themselves live on the SaaS side — as long as you have the IDs, you can get them back.


Tags: #freee #API #disaster-recovery #Python #receipt #accounting-automation

Top comments (0)