Independent Python client built from the current LINE Android 26.11.0 Thrift contracts.
This is an unofficial, experimental project and is not affiliated with or endorsed by LINE/Yahoo Japan. Use it only with accounts and chats you are authorized to test, at conservative request rates.
- Secure QR login with link, terminal QR and SVG output
- Existing access-token login with local session persistence
- PIN approval flow and reusable certificate login
- Windows DPAPI-encrypted local session storage
getProfile,getAllChatMids,getChatsand message history- Dedicated
/SYNC4polling contract - Named 26.11.0
OpTypevalues and persistent sync revisions - Reconnecting background listener and APK-compatible HTTP/2 push mode
- Lazy access to all APK-extracted service paths
- Text
sendMessage, including automatic E2EE v2 encryption retry - Message unsend and
/V4direct/group call route management - Incoming private-chat E2EE v1 and chat/group E2EE v2 text decryption
- Authenticated OBS downloads and verified E2EE media decryption
- E2EE device-key registration and safe register-first key rotation
- LIFF view/token operations and modular Square/OpenChat access
- Explicitly confirmed chat and account-management operations
- Ready-to-run PublicBot (op 25 + 26) and SelfBot (own op 25) helpers
- APK-derived Compact Thrift codec and generated protocol schema
- Pythonic and camelCase-compatible APIs
Maintained by CyberTKR. The modern client is derived from the MIT-licensed line-new-qr-login-api implementation and retains its contributor attribution.
git clone https://github.com/CyberTKR/line-python.git
cd line-python
python -m pip install -e .After installation, either invocation style is available:
python -m lineapi --help
simple-line --helppython -m lineapi registerThe command asks for the region, mobile number, new password, and SMS or voice
PIN. Supported registration regions are TW, TH, JP, KR, and HK. If
LINE requests human verification, the official page opens in Chrome and the
same registration session continues after completion.
The password must contain at least 8 characters and at least three of these
categories: uppercase letter, lowercase letter, number, symbol. Values may
also be supplied with --region, --phone, --display-name,
--verification-method, LINE_REGISTER_PASSWORD, LINE_SIM_HNI, and
LINE_SIM_CARRIER. Registration identity overrides are available through
--registration-application, --registration-user-agent,
LINE_REGISTER_APPLICATION, and LINE_REGISTER_USER_AGENT. The resulting credentials are saved in the configured
.lineapi session and are never printed.
python -m lineapi qr
python -m lineapi qr --app desktopmacThe command prints the login link, renders a terminal QR when supported and
writes lineapi-login-qr.svg. On Windows, credentials and E2EE key-chain
metadata are encrypted for the current user in .lineapi/session.dpapi.
After approval it reads getProfile, prints the logged-in account name,
then performs the initial /SYNC4 sync and persists its revision.
Use an existing access token interactively, or provide it through the
LINE_AUTH_TOKEN environment variable. The token is never printed:
python -m lineapi token
$env:LINE_AUTH_TOKEN = "YOUR_TOKEN"
python -m lineapi token --app desktopwinToken login also verifies the account with getProfile, prints the account
name and performs the initial /SYNC4 sync.
QR and token login support the same selectable LINE application identities:
python -m lineapi qr --app androidsecondary
python -m lineapi qr --app desktopmac
python -m lineapi token --app desktopwin
python -m lineapi token --app chromeAvailable values are androidsecondary, desktopmac, desktopwin, and
chrome.
The selected application header and matching user agent are used for login and
all later requests made by that client. Use the same profile when reopening a
saved session. Android tracks the project's extracted 26.11.0 protocol; the
desktop and Chrome entries are compatibility presets and can be overridden as
LINE releases change.
python -m lineapi profile
python -m lineapi chats
python -m lineapi send TARGET_CHATID "hello"
python -m lineapi listen --app androidsecondary --workers 4The regular LineClient remains the only main client class. from_config()
loads LineConfig and creates the platform-appropriate session storage:
from lineapi import LineClient
client = LineClient.from_config()
profile = client.login_with_qr()For token login, pass an existing token to client.login_with_token(token).
The CLI can instead read LINE_AUTH_TOKEN without placing it in source code.
Direct LineClient construction remains available when full dependency
injection or custom storage is needed:
import os
from lineapi import DpapiFileStorage, FileStorage, LineClient
storage = (
DpapiFileStorage(".lineapi/session.dpapi")
if os.name == "nt"
else FileStorage(".lineapi/session.json")
)
client = LineClient(storage=storage)
profile = client.login_with_qr()
print(profile.display_name)
for chat in client.get_chats():
print(chat.mid, chat.name)
client.send_message("TARGET_CHATID", "hello from Python")
client.unsend_message("MESSAGE_ID")The project uses a modular service and protocol layout:
lineapi/
├── config.py # host, app identity, session and sync settings
├── client.py # messages, chats and operation models
├── cli.py # QR/token login output and initial sync workflow
├── bots.py # PublicBot and SelfBot
├── e2ee.py # E2EE v1/v2 text crypto
├── streaming.py # continuous push transport
├── services/ # endpoint-specific service bindings
└── protocol/ # Compact/MoreCompact codecs
LineConfig.from_env() supports these optional settings:
LINE_API_HOST,LINE_OBS_HOST,LINE_APP_PROFILE,LINE_API_TIMEOUTLINE_APPLICATION,LINE_USER_AGENTfor explicit identity overridesLINE_SESSION_PATH,LINE_PUSH,LINE_SYNC_COUNTLINE_AUTH_TOKENis read only by token login; it is not stored in config.
Example customization:
from lineapi import LineClient, LineConfig
config = LineConfig(push=True, sync_count=100)
client = LineClient.from_config(config)send_message first uses the normal TalkService contract. If LINE returns
E2EE_RETRY_ENCRYPT (82), the client loads the QR-transferred key chain,
obtains the current direct or chat shared key and automatically retries with
APK-compatible AES-GCM E2EE chunks.
Once an E2EE target is observed, the client sends subsequent replies encrypted
immediately instead of spending one request on a known plaintext rejection.
Decoded device keys and short-lived group/public keys are cached in memory to
reduce round trips; they are never written to logs. Bot examples report
per-operation elapsed_ms for local and server-side performance checks.
client.listen(..., workers=4) keeps revision polling ordered while processing
independent command callbacks concurrently under load.
The following methods were tested against a real authorized account, private chat and group:
- Secure QR/PIN login and certificate reuse
getProfilegetAllChatMidsgetChatsgetMessageBoxesgetPreviousMessagesV2WithRequestsync- E2EE text
sendMessage - Incoming private-chat E2EE v1 and chat/group E2EE v2 text decryption
- HTTP/2 push operation delivery and reconnect
The generated schema contains many more APK contracts. A method appearing in
the schema does not mean it already has a high-level Python binding. Legacy
/P4 fetchOps is rejected by the current gateway; use sync.
The optional feature services are exposed on the regular client:
message_bytes = client.download_media(message)
view = client.liff.issue_view("YOUR_LIFF_ID", chat_mid="TARGET_CHAT_MID")
joined = client.square.get_joined_squares()
events = client.square.fetch_chat_events("SQUARE_CHAT_MID")
new_key_id = client.register_e2ee_key()
rotated_key_id = client.rotate_e2ee_key(retire_previous=True)Call routing uses the APK-derived /V4 contract. These methods obtain and
manage LINE call routes; audio/video transport and media rendering remain the
responsibility of the calling application.
client.unsend_message("MESSAGE_ID")
route = client.acquire_call_route("USER_MID")
group = client.get_group_call("TARGET_CHAT_MID")
group_route = client.acquire_group_call_route("TARGET_CHAT_MID")
client.invite_into_group_call("TARGET_CHAT_MID", ["USER_MID"])
call_url = client.create_group_call_url("Team call")
urls = client.get_group_call_urls()
client.update_group_call_url("CALL_URL_ID", "New title")
client.delete_group_call_url("CALL_URL_ID", confirm=True)Encrypted OBS media is authenticated with HMAC-SHA256 before AES-CTR decryption. Invalid or truncated objects raise an exception and are never returned as valid media.
Chat-management methods use explicit target lists. Destructive calls require an additional confirmation argument:
client.invite_into_chat("TARGET_CHAT_MID", ["USER_MID"])
client.cancel_chat_invitation("TARGET_CHAT_MID", ["USER_MID"])
ticket = client.reissue_chat_ticket("TARGET_CHAT_MID")
chat = client.find_chat_by_ticket("TICKET_ID")
client.accept_chat_invitation_by_ticket("TARGET_CHAT_MID", "TICKET_ID")
client.reject_chat_invitation("TARGET_CHAT_MID")
client.kick_from_chat("TARGET_CHAT_MID", ["USER_MID"], confirm=True)
client.leave_chat("TARGET_CHAT_MID", confirm=True)
# Permanently unregisters the current account/device and clears its session.
client.unregister_user_and_device("UNREGISTER")Additional Talk bindings cover contacts, chat creation, announcements, history, followers, configuration and E2EE key lookup:
contact_ids = client.get_contact_ids()
blocked_ids = client.get_blocked_contact_ids()
client.add_friend_by_mid("USER_MID")
chat = client.create_chat("New group", ["USER_MID"])
client.update_chat_name(chat.mid, "Renamed group")
client.update_chat_ticket(chat.mid, enabled=False)
announcements = client.get_chat_announcements(chat.mid)
client.create_chat_announcement(
chat.mid, "Important notice", "https://example.com/notice"
)
recent = client.get_recent_messages(chat.mid, count=50)
revision = client.get_last_op_revision()
followers = client.get_followers(mid="USER_MID")
public_key = client.get_e2ee_public_key("USER_MID", 2, 1)Use account and membership operations only in chats and accounts you control.
The conservative listener repeatedly calls the verified /SYNC4 sync contract,
persists all revisions and reconnects with bounded exponential backoff:
import os
from lineapi import DpapiFileStorage, FileStorage, LineClient
storage = (
DpapiFileStorage(".lineapi/session.dpapi")
if os.name == "nt"
else FileStorage(".lineapi/session.json")
)
client = LineClient(storage=storage)
client.on("operation", lambda op: print(op.revision, op.type_code, op.type_name))
client.on("op:RECEIVE_MESSAGE", lambda op: print(op.message.text if op.message else ""))
client.run_forever()An empty HTTP 410 after a long /SYNC4 wait is treated as normal long-poll
rotation: the saved revision is preserved and a fresh request is opened. Actual
connection failures still use bounded exponential backoff and poll_error.
The supplied Android APK uses a full-duplex HTTP/2 stream at
/PUSH/1/subs. Enable the independently extracted framing with push=True:
Without push=True, polling calls the dedicated /SYNC4 endpoint. General
Talk RPCs continue to use /S4.
client.run_forever(push=True)Or from the CLI:
python -m lineapi listen
python -m lineapi listen --push
python -m lineapi listen --push --self-dot-reply
python -m lineapi servicesThe CLI listener decrypts incoming and outgoing E2EE text before printing it.
--self-dot-reply is an opt-in example automation: when the logged-in account
sends exactly . in a private chat, room or group, it replies hello to the
same exact message target. It never replays historical operations.
Both included bots continuously listen over the APK-compatible push stream and
include hello -> Hello! and ping -> pong commands:
python -m lineapi public-bot
python -m lineapi self-botPublicBothandles op type 25 and 26 separately. For a direct incoming type-26 message it replies to the sender; for a group/room it replies to the original chat.SelfBotaccepts only type 25 messages sent by the logged-in account.
The service commands below are available in both bots. Targeted commands use LINE mentions, so users do not need to copy MIDs:
help | commands
hello | ping | ginfo
call info
call status
call members
call who
call host
call type
call media
call media route
call invite @user
call urls
call url create TITLE
call url info NUMBER
call url update NUMBER TITLE
call url delete NUMBER confirm
unsend NUMBER
contact @user
members
readers
message info
save
react 👍|❤️|😆|😮|😢|😡
unreact
ticket
e2ee status
health
services
save, message info, react, and unreact operate on the message replied
to in LINE. Saved media is written under .lineapi/media/. Contact, member,
reader, and active-call member output uses real LINE mention metadata when the
Relation service can resolve the account names.
call urls prints a numbered list containing every title and real URL ID.
The remaining URL commands accept the displayed number, so copying a raw ID is
not required.
unsend 5 retracts up to five most recent messages sent by the logged-in
account in the current chat. It excludes the command message itself, never
targets another user's messages, and accepts values from 1 to 20.
PublicBot commands are public by default. Pass a control_mids set or change
the example's CONTROL_MIDS value when only selected operators should be able
to use call and unsend commands. SelfBot still accepts only the logged-in
account's own op type 25 messages.
The editable examples keep operation routing and outgoing text in the application file:
from lineapi import LineClient
client = LineClient.from_config()
def bot(op, client):
if op.type_code == 25:
# command behavior and API calls are visible and editable here
...
elif op.type_code == 26:
# received-message behavior is visible and editable here
...
client.listen(bot, push=False) # continuous /SYNC4 loopSee examples/public_bot.py and examples/self_bot.py for complete short
scripts. Replies use resolve_message_text, so plain text and supported E2EE
v1/v2 text messages share the same command path. Both examples print every
outgoing target and text. Edit the explicit command, type-25 and type-26
branches to change behavior. The included ginfo command demonstrates calling
get_chats inside a command before sending group details. Developer logs include compact
self_bot.py:line locations so supported terminals can open the source line.
The examples can be launched directly without installing the package first;
their small _bootstrap.py helper loads the repository source and root session:
cd examples
python self_bot.pystart_polling(push=True) runs the same listener in a managed daemon thread;
call stop_polling() during shutdown. See
docs/protocol-26.11.0.md for the APK-derived frame
layout and service-type mapping.
python -m pip install -e ".[dev]"
python -m pytest -qNever commit .lineapi/, access tokens, QR URLs, PINs, certificates or E2EE
private keys. See SECURITY.md for reporting instructions.
The schema generator reads JADX output from a locally supplied APK and records field IDs, types, method argument/result pairs and service paths. Decompiled APK sources and APK binaries are intentionally not included in this repository.
MIT. The license covers this repository's original source code only. LINE, related trademarks, application binaries and service contracts belong to their respective owners.