Metadata-Version: 2.4
Name: pypaypay
Version: 0.2.0
Summary: Unofficial PayPay (jp.ne.paypay) Python API wrapper — full BFF coverage plus a PayPayOpenPy-compatible high-level surface.
Author: u_pa_
License: MIT
Project-URL: Homepage, https://github.com/amane1201/paypy
Project-URL: Repository, https://github.com/amane1201/paypy
Project-URL: Issues, https://github.com/amane1201/paypy/issues
Keywords: paypay,p2p,wallet,unofficial,api
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-httpx>=0.30; extra == "dev"
Dynamic: license-file

# pypaypay

PayPay の非公式 Python API ラッパーだよ
**高レベル 28 メソッド / raw 192 エンドポイント** — アプリで出来ることはだいたい出来ると思う。

## インストール

```py
pip install pypaypay
```

依存は `httpx` だけ、軽いでしょ

###### インストール名も import 名も `pypaypay`
```py
from pypaypay import PayPay
```

最新版を直で入れたい人はこっち：

```py
pip install git+https://github.com/amane1201/pypaypay
```

ローカルで弄りたい人はこっち：

```py
git clone https://github.com/amane1201/pypaypay
cd pypaypay
pip install -e .
```

## 始める前に確認すること

### これは非公式です
PayPay 株式会社とは縁もゆかりもありません、中の人でもなんでもない。
非公式 API を叩くのは規約的にたぶんアウトなので、**自分のアカウントで、自己責任で**遊んでね。
凍結されても「どんまい」って一緒に言うことしか出来ないよ。

### ログイン失敗は3回まで
超えるとアカウントが一時ロックされる。解除はサポート送りなので、パスワードうろ覚えのまま試行錯誤するのは本当にやめとけ。3回だぞ、3回。

### セッションを作りすぎない
毎回ログインし直すとセッションが積み上がってアカウント凍結の可能性があるっぽい。
**トークンを保存して使い回して**、それだけで寿命が伸びる。

### 日本からしかアクセスできない
海外の VPS からだと普通に弾かれるので、その時はプロキシ挿して。

### Bot 検知がいるらしい
どうやらモバイル API には Bot 検知が存在するっぽくて、引っかかると勝手にログアウトさせられる。
無駄なリクエストを混ぜると回避できるらしい（人間がアプリ触ってたら当然そこそこリクエストするので、それを真似る）。
このモジュールなら `paypay.raw.get_user_profile()` あたりを時々叩いとけばそれっぽくなる。Bot は効率が良いから怪しまれるんだね……。

## Let's Go!

#### example.py

```py
from pypaypay import PayPay

# ① 電話番号＋パスワードで初回ログイン
paypay = PayPay("080-1234-5678", "Gay1919")   # ハイフンはあってもなくてもいい
url = paypay.login()                            # ブラウザで開く URL が返ってくる
print("これ開いて:", url)
paypay.login_confirm(input("URL?: "))           # コールバック URL そのままでも AB-0000 みたいなのだけでも OK

print(paypay.access_token)      # 90日有効、これを保存しろ
print(paypay.refresh_token)     # これも保存しろ、こっちの方が大事まである
print(paypay.device_uuid)       # 登録デバイス管理用、次回これ渡すと URL 入力を省ける
print(paypay.client_uuid)       # 特に気にしなくていい、ランダムでいいらしい

# ② 2回目以降はトークンぶち込んでログイン作業まるごとスキップ
paypay = PayPay(access_token="トークン", refresh_token="リフレッシュ")

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# プロフィールと残高
me = paypay.get_profile()
print(me.name)                  # ユーザー名
print(me.external_user_id)      # 識別用の内部 ID、自分で決めた PayPayID とは別物
print(me.icon)                  # アイコンの URL

bal = paypay.get_balance()
print(bal.all_balance)          # ぜんぶ
print(bal.useable_balance)      # 使える分（ポイント抜き）
print(bal.money_light, bal.money, bal.points)   # ライト / マネー / ポイント

# 履歴とか DM 一覧とか
paypay.get_history(size=20)                     # 支出入の履歴、size は控えめでいい
paypay.get_chat_rooms(size=20)                  # PayPay 内 DM 一覧
paypay.get_chat_room_messages("sendbird_group_channel_なんとか_なんとか")
# ↑ "sendbird_group_channel_" は付けても付けなくても OK、そこは優しい
paypay.get_point_history()

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 送金リンク、みんなこれ目当てでしょ
link = paypay.link_check("https://pay.paypay.ne.jp/Zk8mQ2vR7pL4xN3d")  # ID だけでも OK
print(link.amount)              # 合計金額
print(link.money_light, link.money)             # 内訳
print(link.has_password)        # True ならパスワード必須
print(link.chat_room_id)        # 受け取ったあとお礼 DM 送るやつ
print(link.status)              # PENDING / COMPLETED / REJECTED / FAILED
print(link.order_id)
print(link.sender_external_id)  # 送ってきた人の ExternalID

paypay.link_receive("URL / ID どっちでもOK", "必要ならパスワード 1919", link_info=link)
# ↑ link_info を渡すとリンクチェックをスキップするので速い、連打する人は必ず渡せ
paypay.link_reject("URL / ID どっちでもOK", link_info=link)   # 辞退
paypay.link_cancel("URL / ID どっちでもOK", link_info=link)   # 自分が送ったやつの取り消し

# リンク発行
created = paypay.create_link(amount=100, passcode="1919")
print(created.link)             # ↑で作った URL
print(created.chat_room_id)     # ↑で作ったリンクのチャットルーム ID
print(created.order_id)

# 自分宛て入金 QR
qr = paypay.create_p2pcode()    # amount=int で金額固定も出来る
print(qr.p2pcode)

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 直接送金と DM
send = paypay.send_money(amount=100, receiver_id="友達の external_user_id")
print(send.chat_room_id)
paypay.send_message(send.chat_room_id, "はい100円")
# ここにテンプレ文を流し込めば受け取り連絡が完全自動になる

paypay.set_money_priority(paypay_money=False)   # False=ライト優先 / True=マネー優先

# ユーザー検索 → チャットルーム
user = paypay.search_p2puser("amane1201")                    # PayPayID でグローバル検索
user = paypay.search_p2puser("あまね", is_global=False, order=0)  # フレンドなら表示名でも
print(user.name, user.icon, user.external_user_id)
room = paypay.initialize_chatroom(user.external_user_id)
paypay.send_message(room.chatroom_id, "テスト")

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 加盟店 QR と出金
info = paypay.get_barcode_info("https://qr.paypay.ne.jp/............")
print(info.amount)              # 金額指定なしの請求リンクだと None
print(info.merchant_name)
print(info.external_user_id)    # send_money の receiver_id に流用できる

paypay.pay_qr_code("https://qr.paypay.ne.jp/............")   # amount=int で金額指定も
paypay.cashout_to_paypaybank(100)                           # PayPay 銀行へ出金
```

長いけど使い方はコメントに全部書いといたから、ここだけ読めばだいたい動かせる。

### dict 探し地獄からの卒業

返り値は全部 `dict` のサブクラス。よく使うやつは `.属性` で取れるようにしてあるので、
`payload["walletSummary"]["usableBalance"]["moneyLight"]["amount"]` みたいな**深すぎる階層をスコップで掘る作業**はもう要らない。

```py
me = paypay.get_profile()
me.name          # -> 属性アクセス（欲しいやつはだいたいこれで出る）
me["nickName"]   # -> dict アクセス（生キー）
me.raw           # -> {...} フル dict、生で見たい時用
```

無い属性は例外じゃなく `None` を返すので `if me.icon:` で普通に判定できる。優しい。

## 全部叩ける — `paypay.raw`

高レベルに無いエンドポイントも `paypay.raw.<メソッド名>` で **192 本ぜんぶ**呼べる。
パラメータ名はデコンパイル済のソースから機械抽出したのがキーワード引数で並ぶので IDE 補完が効く。

```py
# BFF 系
paypay.raw.get_user_profile(includeUserScore=True)
paypay.raw.get_kyc_display_info(requestKycTypes=["IDENTIFICATION"])
paypay.raw.get_gv_list(statusFilter="ACTIVE", pageSize=20)
paypay.raw.follow_channel(channelId="xxxx", type="STORE")
paypay.raw.like_feed(feedId="yyyy")

# P2P 系（グループ機能とか割り勘とか）
paypay.raw.create_group_pay()
paypay.raw.get_p2p_friends(pageSize=50)
paypay.raw.block_user(externalUserId="...")

# 抽出漏れフィールドは **extra で足せる、アプリが更新されても待たなくていい
paypay.raw.change_user_profile(nickName="太郎", **{"someNewField": True})
```

全メソッド一覧が欲しかったら：

```py
python -c "from pypaypay.raw import RawAPI; print('\n'.join(sorted(m for m in dir(RawAPI) if not m.startswith('_'))))"
```

`help(paypay.raw.get_gv_list)` すれば元のエンドポイントが docstring に書いてある。迷ったら叩け。

## もう少し詳しく

### ログインのやり方いろいろ

```py
# ① 電話番号＋パスワード（一番普通、ワンタイム URL を踏む）
paypay = PayPay("080-1234-5678", "Gay1919")
paypay.login_confirm(input(paypay.login() + " ← 開いて、戻ってきた URL: "))

# ② 登録済みデバイス UUID がある（URL 入力を省ける）
paypay = PayPay("080-1234-5678", "Gay1919", "登録済みのデバイスUUID", proxy=None)
paypay.login()

# ③ トークンだけ持ってる（ログイン作業まるごとスキップ）
paypay = PayPay(access_token="...", refresh_token="...")

# 手動でリフレッシュしたい時はこれ
paypay.token_refresh("ここにリフレッシュトークン")
print(paypay.access_token)   # ← 更新後のやつが入ってる
print(paypay.refresh_token)
```

`refresh_token` を渡してあると **401 が来たら勝手に叩き直してリトライしてくれる**ので、初回だけ通せば長期間放置できる。えらい。

1アカウント分の身分証は 電話番号 / パスワード / Device_UUID / Client_UUID / アクセストークン / リフレッシュトークン の6点セット。
このうち実際に握っとくべきなのは**アクセストークンとリフレッシュトークンと Device_UUID** の3つで、DB に書き込むならこの3つ。
アクセストークンは **90日** 有効なので、一度通せばしばらく忘れてても動いてる。

###### 電話番号のハイフンはあってもなくても OK
クライアント UUID は常にランダムでいいみたい。アクセストークンをぶち込んでる場合はデバイス UUID もランダムでいいみたい。適当。

### 設定いろいろ

```py
paypay = PayPay(
    access_token="...",
    proxy="127.0.0.1:8888",   # str なら http:// は省略可、dict でもいける
    timeout=20,
    sandbox=False,            # True で stg 環境に向く（使う機会たぶん無い）
)
```

### PayPay の DM

送金・受け取り履歴のところに生えてるチャット。取引で絡んだ相手にだけ喋れる仕様。
リンクチェックすると DM 送信用のチャットルーム ID が付いてくるので、受け取ったら勝手にお礼を投げる、みたいなのが組める。
納品物の詳細を流すもよし、雑に一言だけ返すもよし。無言で回収するより印象は良い。

ID は `sendbird_group_channel_なんとか_なんとか` の形式だけど、`send_message` に渡す時は `sendbird_group_channel_` の部分は無くても OK。

#### もう少し効率化

チャットルーム ID がリンクチェック時しか貰えないのはつらいけど、検索と組み合わせれば何とかなる：

```py
user = paypay.search_p2puser("amane1201")                 # PayPayID で検索
room = paypay.initialize_chatroom(user.external_user_id) # チャットルーム ID を取得
paypay.send_message(room.chatroom_id, "テスト")           # 送信
```

表示名で探す場合は `is_global=False`（表示名検索はフレンドしか出てこない）。
逆にフレンド検索に PayPayID は使えない（NotFound が返る）。統一しろ。

ただし**ユーザー ID 検索はすぐレート制限に入る**ので、連打前提の運用には向かない。基本はリンク受け取り時に降ってくる ID を使って、検索は最後の手段くらいの気持ちで。

#### もちろん DM じゃなく直接送金も出来る

```py
user = paypay.search_p2puser("あまね")
paypay.send_money(100, user.external_user_id)
```

送金は ExternalID だけで済むので、DM より簡単。金だけ投げるのは簡単ってコト。

## エラー

失敗したら例外が飛ぶ。種類で分けて掴まえられるよ：

```py
from pypaypay import APIError, TokenExpiredError, RateLimitedError, LinkAlreadyClaimed

try:
    paypay.link_receive("Zk8mQ2vR7pL4xN3d")
except LinkAlreadyClaimed:
    print("誰かに先を越された")
except RateLimitedError:
    print("落ち着け")
except TokenExpiredError:
    print("トークン死んだ、refresh して")
except APIError as e:
    print("なんか失敗:", e.status, e.code, e.payload)
```

```
PayPayError
├── AuthError
│   └── TokenExpiredError          # 401 / トークン失効 / S0001
├── APIError                       # BFF が resultCode != S0000
│   ├── .status                    #   HTTP ステータス
│   ├── .code                      #   BFF の resultCode
│   └── .payload                   #   レスポンス全文（困ったらこれ print）
├── RateLimitedError               # 429
├── LinkPasscodeRequired           # 受取パスコード必須
└── LinkAlreadyClaimed             # 受取 / 拒否済み
```

処理済みのリンクや無効なリンクを触るとちゃんとエラーにしてあるので、握りつぶさず判別に使って。

## トークンをどう手に入れるか

`login()` → `login_confirm()` で普通に取れる。それが面倒なら：

- **mitmproxy 派**：PayPay アプリのログイン通信を横取りして `bff/v2/oauth2/token` のレスポンスから抜く
- **API 手組み派**：`paypay.raw.oauth2_par(...)` → SMS OTP → `paypay.raw.oauth2_token(...)` を自前で組む（PKCE 必要）

どの道 `refresh_token` さえ生きてれば以後は自動更新なので、初回さえ突破すれば勝ち。

## 余談：どうやって作ったか

APK を [JADX](https://github.com/skylot/jadx) でデコンパイルして、ひたすら読んだ。

- `HeaderInterceptor` 相当のところ → 送ってるヘッダー、`Client-UUID` / `Device-UUID` / `Client-OS-Version` の作り方が丸見えだった
- BFF の実装クラス（難読化後は `C19344p` みたいな味気ない名前）→ **192 本のエンドポイントとパラメータ名が全部そこ**にあった
- OAuth2 の client_id は本番用と sandbox 用が定数で埋まってた、いいのそれで

難読化でメソッド名は潰れてるけどエンドポイント文字列とフィールド名は生で残ってるので、そこを正規表現でさらって `pypaypay/raw.py` を自動生成してる。機械生成なのでメソッド名がちょっとダサいのは勘弁。

アプリのバージョンが変わったら、新しい APK をデコンパイルして：

```py
py -3 scripts/extract_endpoints.py       # BFFImpl から endpoint → param 抽出
py -3 scripts/codegen.py                 # pypaypay/raw.py を再生成
```

パスが違うなら `PAYPAY_BFF_SRC=/path/to/BFFImpl.java` を頭に付けて。
デコンパイルしたのは jp.ne.paypay 5.49.0 (Android)、User-Agent もそれに合わせてある。

## さらに余談

なんで pypaypay なの？って思った？実は深い理由が……無い。py + PayPay で pypaypay、以上。
本当は `paypy` にしたかったんだけど、PyPI の `paypy` は 2012年で時が止まってる別人の遺跡が居座ってて取れなかった。14年前の決済ゲートウェイに負けた。
読み方は「ペイパイ」でも「ペイピー」でも好きにしてくれ。作者も日によって変わる。

あと高レベル API は PayPayOpenPy 互換の名前にしてあるので、乗り換えは import 書き換えるだけでだいたい動く。優しさ。

## コンタクト / 貢献

バグとか「このメソッド動かんぞ」は GitHub の Issues に投げて。
PR も歓迎、エンドポイント追加はスクリプト再生成なので気軽にどうぞ。

## ライセンス

MIT — [LICENSE](LICENSE) 見て好きに使っていいよ。
PayPay の商標とかサービス自体は当然 PayPay 株式会社のもの、このライブラリは無関係の第三者製です。
