LINE API
Why LINE in Thailand
LINE has over 50 million users in Thailand. Most Thai people communicate with businesses on LINE rather than email or SMS. The problem JongYang solves — replacing LINE DM back-and-forth — requires LINE integration:
- Customers already have LINE — they don't need a new account
- Booking confirmations arrive in the same app they use for everything
- Reminders ("Your appointment is tomorrow!") are seen immediately
- Email would go unread; SMS costs money per message
Two Separate LINE Products
JongYang uses two different LINE products with different purposes:
- LINE Login — Like "Login with Google" for LINE. After booking, the customer can connect their LINE account. We get their LINE user ID (a unique string like
U4af4980629...), which we store in the booking record. - LINE Messaging API — A channel that lets us send messages to LINE users. We use the LINE user ID from LINE Login to push notifications directly to the customer's LINE app.
LINE Login: The OAuth2 Flow
OAuth2 is an industry-standard protocol for "login with provider X" flows. Here's what happens step by step:
1. Customer clicks "Connect LINE" after booking confirmation page loads
2. Browser redirects to LINE authorization URL:
https://access.line.me/oauth2/v2.1/authorize
?response_type=code
&client_id=2010471965 ← LINE_LOGIN_CHANNEL_ID
&redirect_uri=https://jongyang.xyz/line-callback
&scope=profile
3. Customer sees LINE's consent screen, clicks "Allow"
4. LINE redirects browser back to:
https://jongyang.xyz/line-callback?code=abc123&state=...
5. Next.js line-callback page extracts the ?code= parameter
Sends it to FastAPI: POST /api/bookings/{id}/line-connect
Body: {"code": "abc123", "redirect_uri": "https://jongyang.xyz/line-callback"}
6. FastAPI calls exchange_code("abc123", redirect_uri):
POST https://api.line.me/oauth2/v2.1/token
grant_type=authorization_code
code=abc123
redirect_uri=...
client_id=...
client_secret=...
← Response: {"access_token": "eyJ..."}
7. FastAPI calls LINE profile API:
GET https://api.line.me/v2/profile
Authorization: Bearer eyJ...
← Response: {"userId": "U4af4980629abc123...", "displayName": "สมชาย ใจดี"}
8. FastAPI stores userId in booking.customer_line_user_id
Returns {"connected": true, "customer_line_user_id": "U4af..."}
9. Customer sees "Connected!" message on the page
async def exchange_code(code: str, redirect_uri: str) -> str:
"""Exchange a LINE Login authorisation code for the user's LINE user ID."""
async with httpx.AsyncClient() as client:
# Step 1: Exchange code for access token
token_r = await client.post(
"https://api.line.me/oauth2/v2.1/token",
data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirect_uri,
"client_id": settings.LINE_LOGIN_CHANNEL_ID,
"client_secret": settings.LINE_LOGIN_CHANNEL_SECRET,
},
)
token_r.raise_for_status()
access_token = token_r.json()["access_token"]
# Step 2: Get the user's LINE user ID
profile_r = await client.get(
"https://api.line.me/v2/profile",
headers={"Authorization": f"Bearer {access_token}"},
)
profile_r.raise_for_status()
return profile_r.json()["userId"] # "U4af4980629abc123..."
LINE Messaging API: Sending Push Notifications
Once we have a customer's userId, we can push messages to them directly in LINE:
_PUSH_URL = "https://api.line.me/v2/bot/message/push"
async def push_text(user_id: str, text: str) -> None:
"""Send a plain-text push message to a LINE user."""
async with httpx.AsyncClient() as client:
r = await client.post(
_PUSH_URL,
headers={"Authorization": f"Bearer {settings.LINE_CHANNEL_ACCESS_TOKEN}"},
json={
"to": user_id, # the LINE user ID
"messages": [{"type": "text", "text": text}],
},
)
r.raise_for_status()
def msg_booking_confirmed(shop_name, service_name, booking_date, start_time, booking_id):
service_line = f"บริการ: {service_name}\n" if service_name else ""
return (
f"✅ ยืนยันการจองแล้ว!\n"
f"ร้าน: {shop_name}\n"
f"{service_line}"
f"วันที่: {booking_date} เวลา: {start_time}\n"
f"หมายเลขจอง: ...{booking_id[-8:]}"
)
Reminder Scheduling
APScheduler runs the reminder task every 30 minutes. It sends reminders at T-24h and T-2h before the appointment:
_BKK_OFFSET = timedelta(hours=7) # Bangkok is UTC+7
async def run_reminder_task(session, now=None):
if now is None:
now = datetime.now(timezone.utc)
now_bkk = now + _BKK_OFFSET # convert UTC to Bangkok time
today = now_bkk.date()
tomorrow = today + timedelta(days=1)
# T-24h: confirmed bookings for TOMORROW, reminder not yet sent
rows_24h = (await session.execute(
select(Booking).where(
Booking.status == "confirmed",
Booking.booking_date == tomorrow,
Booking.reminder_24h_sent == False, # not sent yet
Booking.customer_line_user_id.is_not(None), # must have LINE connected
)
)).scalars().all()
# T-2h: bookings today with start_time in [now+2h, now+3h)
window_2h_start = (now_bkk + timedelta(hours=2)).time()
window_2h_end = (now_bkk + timedelta(hours=3)).time()
# ...
for booking in rows_24h:
await push_text(booking.customer_line_user_id, msg_reminder(..., hours=24))
booking.reminder_24h_sent = True # ← prevents sending again
The reminder_24h_sent and reminder_2h_sent boolean columns on the Booking model are set to True after sending. This prevents the scheduler from sending the same reminder twice if it runs every 30 minutes.
Bangkok timezone handling is important: booking times are in local Thai time. The scheduler runs in UTC internally, so we add 7 hours (_BKK_OFFSET) to convert UTC to Bangkok time before comparing with booking dates and times.
🧠 Self-Check Quiz
1. What is the OAuth2 "authorization code" and why do we need to exchange it for an access token?
2. What is the LINE user ID and why is it important to store it in the booking?
U4af4980629...) is a unique, permanent identifier for a LINE account on our specific LINE channel. We store it in booking.customer_line_user_id so we can send push notifications to that specific person. Without it, we could create a booking but have no way to reach the customer after payment. It's the "address" for LINE messages.3. What are the two separate LINE products used, and what does each do?
4. Why do reminders have reminder_24h_sent and reminder_2h_sent boolean flags?
5. Why does the reminder task add 7 hours to UTC time when calculating which bookings to remind about?
datetime.now(timezone.utc) returns UTC. If it's currently 23:00 UTC on Monday, in Bangkok it's already 06:00 Tuesday. Adding 7 hours gives the Bangkok "now" — so "tomorrow" and "today" are calculated correctly for Thai customers. Without this offset, reminders would fire at the wrong local times.