クラウド会計ソフト freee の APIの使い方
私はクラウド会計ソフトに freee を使用しています。
最近、領収書のファイル・アップロードが面倒になってきましたので、APIで自動化しようと思い、調べてみました。
HTTPプロトコルを使用するだけなので、使用する言語はなんでも良いです。このブログでは、JavaScript
とPython
を使用しています。
freeeのAPIキーについて
SaaSのAPIは、管理画面でAPIキーを払い出してAPIを叩くのが一般的かと思います。(例: OpenAI API)
一方で、freeeの場合は、OAuth2のアクセス・トークンを払いだしてAPIを叩く方式になっています。
予備知識として、OAuth2の認可フローを理解しておくと良いでしょう。
アプリケーションを作成する
最初に、freeeの開発者ページの今すぐアプリを作成から、OAuth2のアプリケーションを作成します。
アプリケーションの作成が完了すると、図のようにアクセス・トークンの取得に必要な情報が表示されます。
Client ID
、Client Secret
、Webアプリ認証用URL
はあとで使用しますので、メモしてください。もちろん、あとで閲覧することも可能です。
認可コードをゲットする
OAuth2
では、いきなりアクセス・トークンは払い出せません。前段階として、いったん、認可コードという、有効期限の短い(10分)エフェメラルなコードを取得します。
Webアプリ認証用URLをWebブラウザーから開きます。
Gmailなどのソーシャル・ログインでお馴染みの、OAuth2の認可フローが開始しますので処理を進めます。最後に認可コードが画面に表示されて終わりです。
アクセス・トークンをゲットする
次に、払い出された認可コードを使用して、アクセス・トークンを取得します。
言語は何でも良いですが、ここではJavaScript(Node.js)を使用します。
.env
に必要な環境変数をセットします。
AUTHORIZATION_CODE
に取得した認可コードをセットします。
CLIENT_ID
とCLIENT_SECRET
は、freeeの開発者ページで作成したアプリケーションのページから確認してください。
TOKEN_URL=https://accounts.secure.freee.co.jp/public_api/token
REDIRECT_URI=urn:ietf:wg:oauth:2.0:oob
# The client ID and secret from your app settings.
CLIENT_ID=6057...580
CLIENT_SECRET=R9Z-...L9Wep5KA
# Authorization Code by OAuth2.0
AUTHORIZATION_CODE=dcf...fd6576c696a
get-token.js
を作成し、実行します。
// ← make sure your package.json has "type": "module"
import {URLSearchParams} from 'url';
import dotenv from 'dotenv';
dotenv.config();
/**
* 1) Exchange a one-time authorization code for access + refresh tokens
* @param {string} code
* @returns {Promise<{ access_token: string, refresh_token: string, expires_in: number }>}
*/
export async function fetchAccessToken(code) {
const body = new URLSearchParams({
grant_type: 'authorization_code',
redirect_uri: process.env.REDIRECT_URI,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
code,
});
const res = await fetch(process.env.TOKEN_URL, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: body.toString(),
});
if (!res.ok) {
const errText = await res.text();
throw new Error(`Token exchange failed (${res.status}): ${errText}`);
}
return res.json();
}
// ——— USAGE EXAMPLE ———
if (import.meta.url === `file://${process.argv[1]}`) {
(async () => {
try {
// Step 1: one-time code → tokens
const {access_token, refresh_token, expires_in} =
await fetchAccessToken(process.env.AUTHORIZATION_CODE);
console.log('▶ Access token:', access_token);
console.log('▶ Refresh token:', refresh_token);
console.log(`▶ Expires in: ${expires_in}s`);
} catch (err) {
console.error(err);
}
})();
}
成功すると、めでたくアクセス・トークンが表示されます。
これで、あとはお好みのAPIをバシバシ叩くだけです。
APIを叩く
freeeが公開するAPI一覧から、使いたいAPIのエンドポイントを探してください。
ファイル・アップロードAPI
ここではレシートファイル(PDF)のアップロードAPIのサンプルをご紹介します。
レシートのアップロードは、Receiptsファイルボックス(証憑ファイル)を使用します。
エンドポイントはこちらになります。
アプリの権限の設定
お気づきかもしれませんが、先ほど払い出したアクセス・トークンではファイル・アップロードAPIのアクセス権限がありません。
権限を付与するには、freeeのアプリ管理の権限設定タブから、使用したいAPIをチェックする必要があります。
ここでは、ファイルボックスにチェックを入れる必要があります。
変更したら、画面右上の下書き保存ボタンを押すのを忘れないでください。
設定変更が完了したら、認可コードの発行から再度やり直してアクセス・トークンの再発行をしてください。
事業所ID
APIの利用にはアクセス・トークンの他に、事業所IDも必要です。
それにはAPI一覧のページから事業所一覧の取得のAPIを叩いて確認します。
まず、Authorize
ボタンをクリックし、アクセス・トークンをセットしてください。Swaggerと思いますが、これでWeb画面からAPIをバシバシと叩くことができます。
Company /api/1/companies 事業所一覧の取得を叩きます。
実行すると、事業所一覧がJSONレスポンスで表示されます。JSONのidが事業所IDになります。
間違えやすいですが、company_number
ではありません。id
です。
Pythonコーディング
Node.jsでも良いですが、ここではPythonのサンプルをご紹介します。
といっても、ChatGPTに書いていただいたものです。非同期処理で書いてもらいました。
環境変数をセットします。
- COMPANY_ID 事業所ID (JSONの id)
- ACCESS_TOKEN アップロード・ファイル
Pythonのコードです。
#
# export COMPANY_ID=123456
# export ACCESS_TOKEN="your_access_token_here"
#
# python upload_receipt.py ./path/to/file.pdf --description "Sample receipt"
#
import os
import sys
import httpx
import asyncio
import argparse
from typing import Optional, Dict
async def upload_single_pdf(
file_path: str,
endpoint_url: str,
company_id: int,
description: Optional[str],
client: httpx.AsyncClient
) -> Dict:
"""
Upload a single PDF file along with company_id and description.
"""
filename = os.path.basename(file_path)
data = {"company_id": str(company_id)}
if description:
data["description"] = description
try:
with open(file_path, "rb") as f:
response = await client.post(
endpoint_url,
data=data,
files={"receipt": (filename, f, "application/pdf")}
)
response.raise_for_status()
return {"file": filename, "response": response.json()}
except FileNotFoundError:
return {"file": filename, "error": "File not found"}
except httpx.HTTPStatusError as e:
return {"file": filename, "error": f"HTTP {e.response.status_code}: {e.response.text}"}
except httpx.RequestError as e:
return {"file": filename, "error": str(e)}
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Upload a single PDF file with company_id & optional description."
)
p.add_argument(
"file",
metavar="PDF",
help="Path to the PDF file to upload"
)
p.add_argument(
"--description",
type=str,
default=None,
help="メモ (max 255 chars)"
)
return p.parse_args()
async def _main_async(
file_path: str,
company_id: int,
description: Optional[str],
access_token: str
):
"""
Upload a single PDF file to the freee API asynchronously.
"""
endpoint_url = "https://api.freee.co.jp/api/1/receipts"
headers = {"Authorization": f"Bearer {access_token}"}
async with httpx.AsyncClient(timeout=10.0, headers=headers) as client:
return await upload_single_pdf(
file_path, endpoint_url, company_id, description, client
)
def main():
"""
Main function to handle the upload process.
"""
company_id_str = os.getenv("COMPANY_ID")
if not company_id_str:
print("❌ COMPANY_ID environment variable is not set.", file=sys.stderr)
sys.exit(1)
try:
company_id = int(company_id_str)
except ValueError:
print(f"❌ COMPANY_ID must be an integer (got '{company_id_str}').", file=sys.stderr)
sys.exit(1)
# 2) ACCESS_TOKEN check
access_token = os.getenv("ACCESS_TOKEN")
if not access_token:
print("❌ ACCESS_TOKEN environment variable is not set.", file=sys.stderr)
sys.exit(1)
# 3) CLI args
args = parse_args()
file_path = args.file
# 4) Run async upload
result = asyncio.run(
_main_async(file_path, company_id, args.description, access_token)
)
# 5) Report outcome
if "error" in result:
print(f"❌ Upload failed for {result['file']}: {result['error']}", file=sys.stderr)
sys.exit(1)
else:
print(f"✅ Uploaded {result['file']}:")
print(result["response"])
if __name__ == "__main__":
main()
実行します。
引数にアップロードするPDFファイルを指定します。
freee_upload_single.py my-amazon-receipt.pdf
うまく行きましたでしょうか? freeeのサイトから、ファイルが正しくアップロードされたのか確認してください。
以上で終わりです。
補足: リフレッシュ・トークンの存在
アクセス・トークンは6時間で有効期限が切れます。そのためリフレッシュ・トークンを使用して、アクセス・トークンを再度払い出す必要があります。このとき、認可コードは不要です。
また、リフレッシュ・トークンも一緒に再発行されるので、本番ではリフレッシュ・トークンをローテーションする仕掛けも必要になります。
認可コード < アクセス・トークン < リフレッシュ・トークン、の順に有効期限が長くなるわけです。流出リスクの高いトークンの有効期限を短くしつつ、より流出リスクの低いトークンで発行するという仕組みです。
特に説明は不要かと思いますので、ソースコードだけ載せます。
// ← make sure your package.json has "type": "module"
import {URLSearchParams} from 'url';
import dotenv from 'dotenv';
dotenv.config();
/**
* 2) Use your stored refresh token to mint a fresh access token
* @param {string} refreshToken
* @returns {Promise<{ access_token: string, refresh_token: string, expires_in: number }>}
*/
export async function refreshAccessToken(refreshToken) {
const body = new URLSearchParams({
grant_type: 'refresh_token',
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: refreshToken,
});
const res = await fetch(TOKEN_URL, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: body.toString(),
});
if (!res.ok) {
const errText = await res.text();
throw new Error(`Refresh token failed (${res.status}): ${errText}`);
}
return res.json();
}
// ——— USAGE EXAMPLE ———
if (import.meta.url === `file://${process.argv[1]}`) {
(async () => {
try {
// Step 2: later, renew access
const {access_token: newAt, refresh_token: newRt} =
await refreshAccessToken(refresh_token);
console.log('▶ New access token:', newAt);
console.log('▶ New refresh token:', newRt);
} catch (err) {
console.error(err);
}
})();
}
次回はLLMを使用して、PDFファイルからメタデータを抽出する方法をご紹介する予定です。
Discussion