← Payments & Webhooks 08 / 18 Next: React →

LINE API

Module 3 · External Services · ⏱ ~25 min
Why this matters: In Thailand, LINE is what WhatsApp is in Europe — almost everyone uses it. JongYang integrates two LINE products: LINE Login (so customers can identify themselves) and LINE Messaging API (so they receive booking confirmations and reminders on LINE, not email).

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:

Two Separate LINE Products

JongYang uses two different LINE products with different purposes:

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
python — backend/app/services/line_login.py
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:

python — backend/app/services/line_notify.py
_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:

python — backend/app/tasks/scheduler.py (reminder logic)
_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?

The authorization code is a temporary, single-use code that LINE gives the browser after the customer approves access. The browser can't be trusted with the actual access token (it could be intercepted from the URL). So LINE gives a short-lived code instead. Our server then exchanges the code for a real access token via a server-to-server call (not through the browser). This keeps the access token secret.

2. What is the LINE user ID and why is it important to store it in the booking?

The LINE user ID (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?

LINE Login — OAuth2-based identity. Lets customers prove who they are via their LINE account, giving us their LINE user ID. LINE Messaging API — a messaging channel that lets our server push messages to LINE users. We use the LINE user ID from LINE Login to push booking confirmations and reminders through the Messaging API.

4. Why do reminders have reminder_24h_sent and reminder_2h_sent boolean flags?

The reminder task runs every 30 minutes. Without flags, it would send the 24h reminder multiple times (once for each run while the booking is still in the T-24h window). Setting the flag to True after sending ensures each reminder is sent exactly once, regardless of how many times the scheduler runs.

5. Why does the reminder task add 7 hours to UTC time when calculating which bookings to remind about?

Booking dates and times are stored in local Bangkok time (UTC+7), but Python's 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.