A successful LINE MINI App purchase reservation does not mean that the customer completed the payment.
The final source of truth is the purchaseComplete webhook. But what happens when your endpoint is unavailable, a deployment breaks webhook processing, or the event reaches your server but the business transaction fails?
LINE provides an event history API that can recover purchase webhooks from the previous seven days. The recovery process still needs careful pagination, reconciliation, and idempotency to avoid granting the same item twice.
Reservation success and purchase completion are different events
LINE MINI App in-app purchases use a multi-step flow.
Your server first reserves the purchase:
POST https://api.line.me/iap/v1/product/reserve
Enter fullscreen mode Exit fullscreen mode
LINE returns an orderId, but the customer can still:
- Close the MINI App
- Cancel the app-store payment
- Lose network connectivity
- Fail to finish the payment flow
The digital item or entitlement should therefore be granted only after processing a purchaseComplete event.
These are four separate questions:
Question Evidence to trust Did the reservation succeed? Reserve response, savedorderId, and x-line-request-id
Did the purchase complete?
A purchaseComplete event
Did the live webhook reach your endpoint?
Raw request log and webhook-processing record
Was the entitlement granted exactly once?
An idempotency record keyed by orderId
Store recovery data when reserving the purchase
A recovery job cannot reconcile an order if the reservation was never recorded.
Store at least:
- Your internal checkout ID
- LINE’s
orderId - The
x-line-request-idresponse header - Reservation timestamp
- Expected product or entitlement
- Current purchase state
- Whether a
purchaseCompleteevent has already been applied
Alert when a reservation remains unresolved beyond the expected checkout duration.
Do not mark it as paid automatically, and do not wait until the seventh day to investigate. The event history API only covers the preceding seven days.
Query a fixed recovery window
Use the official event history endpoint:
curl --get "https://api.line.me/iap/v1/webhook/events" \
-H "Authorization: Bearer ${LINE_CHANNEL_ACCESS_TOKEN}" \
--data-urlencode "startEpochSeconds=1784678400" \
--data-urlencode "endEpochSeconds=1784700000" \
--data-urlencode "pageSize=100" \
--data-urlencode "status=FAILED"
Enter fullscreen mode Exit fullscreen mode
The timestamps above are example values for a window on July 22, 2026. Generate UTC epoch seconds from the actual incident start and end times.
The status filter describes webhook delivery:
-
FAILED: LINE could not successfully deliver the webhook -
SUCCESS: LINE successfully delivered it
It does not describe whether the customer’s payment succeeded or failed.
When the problem occurred after your endpoint accepted the request, querying only status=FAILED may not be sufficient. In that case, omit the filter and reconcile every event within the incident window.
Keep pagination parameters stable
The history endpoint returns at most 100 records per page and may include a nextCursor.
For every subsequent page, keep these values unchanged:
startEpochSecondsendEpochSecondspageSizestatus
Only the cursor should change.
const baseUrl = "https://api.line.me/iap/v1/webhook/events";
const fixedQuery = {
startEpochSeconds: "1784678400",
endEpochSeconds: "1784700000",
pageSize: "100",
status: "FAILED",
};
let cursor;
do {
const query = new URLSearchParams(fixedQuery);
if (cursor) {
query.set("cursor", cursor);
}
const response = await fetch(`${baseUrl}?${query}`, {
headers: {
Authorization: `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}`,
},
});
if (!response.ok) {
throw new Error(
`LINE event history request failed: ${response.status}`,
);
}
const page = await response.json();
for (const record of page.events) {
if (record.event.type === "purchaseComplete") {
await applyPurchaseOnce(
record.event.orderId,
record.event,
);
}
}
cursor = page.nextCursor ?? undefined;
} while (cursor);
Enter fullscreen mode Exit fullscreen mode
Changing the time range or filters during pagination can create overlaps or gaps in the recovery result.
Use the same handler for live and recovered events
Do not create a separate entitlement implementation exclusively for recovered events.
Both paths should call the same business handler:
Live webhook ────────┐
├── applyPurchaseOnce(orderId, event)
Event history API ───┘
Enter fullscreen mode Exit fullscreen mode
The handler should atomically:
- Insert an idempotency record keyed by
orderId - Stop if that key already exists
- Validate the expected product and order state
- Grant the entitlement
- Mark the purchase as completed
- Commit everything in one transaction
If the live webhook was already applied, the recovered event should become a no-op.
Conceptually:
async function applyPurchaseOnce(orderId, event) {
return database.transaction(async (transaction) => {
const inserted = await transaction.insertIdempotencyKey(orderId);
if (!inserted) {
return {
status: "already_applied",
orderId,
};
}
await transaction.grantEntitlement({
orderId,
productId: event.productId,
});
await transaction.markPurchaseCompleted(orderId);
return {
status: "applied",
orderId,
};
});
}
Enter fullscreen mode Exit fullscreen mode
The exact database API will differ, but the idempotency record and entitlement update must share the same transaction boundary.
Reconcile four sources of evidence
For a fixed incident window, compare:
- Reserved
orderIdvalues that remain unresolved - Live webhook request and processing logs
- Event history records returned by LINE
- Idempotency and entitlement records in your database
Classify every reservation as one of:
- Completed and applied
- Completed but already applied
- Incomplete or canceled
- Missing and requiring manual investigation
Record the following information in the incident report:
- Exact UTC start and end timestamps
- Applied filters
- Number of pages processed
- Number of events examined
- Recovered
orderIdvalues - Duplicate events skipped
- Last successful recovery time
Continue verifying live webhook signatures
The history API uses a channel access token. It does not recreate the original HTTP webhook request and should not be expected to contain its original signature header.
For live deliveries, continue verifying LINE’s x-line-signature against the raw request body using the channel secret.
Do not parse and serialize the JSON again before verification. Signature validation must use the exact raw bytes received by the endpoint.
Recovered history events should be marked with a source such as:
{
"source": "line_event_history",
"orderId": "example-order-id"
}
Enter fullscreen mode Exit fullscreen mode
This makes audit logs distinguishable without introducing a second entitlement path.
Important limitations
The event history API is a recovery source, not a replacement for live webhook monitoring.
Keep these boundaries in mind:
- The lookback period is seven days, not a permanent ledger
- Each page returns at most 100 records
-
status=FAILEDreports delivery failure, not purchase failure - A successfully delivered webhook may still fail inside your application
- Recovered events may duplicate events already processed live
- Refund history may follow a different API lifecycle
- Your own reservation and entitlement records must be retained longer than seven days
Schedule reconciliation frequently enough that a weekend outage cannot age out of the recovery window.
Recovery checklist
Before running the job:
- [ ] Confirm the exact UTC incident window
- [ ] Save the original query parameters
- [ ] Confirm the channel access token is available server-side
- [ ] Back up or snapshot the unresolved reservation list
- [ ] Verify that
applyPurchaseOnceis idempotent
While processing:
- [ ] Keep filters unchanged across pages
- [ ] Change only
cursor - [ ] Process only expected event types
- [ ] Deduplicate by
orderId - [ ] Record recovered and skipped orders
- [ ] Stop on authentication or schema errors
After processing:
- [ ] Reconcile every reservation in scope
- [ ] Confirm entitlements were granted exactly once
- [ ] Record the last successful recovery checkpoint
- [ ] Investigate records that remain unclassified
- [ ] Confirm live webhook monitoring is healthy again
Final takeaway
A successful purchase reservation is not proof of payment, and a missed live webhook does not need to become a permanent lost order.
A reliable recovery workflow should:
- Query LINE’s event history before the seven-day boundary
- Keep the incident window fixed during pagination
- Replay
purchaseCompleteevents through the live business handler - Use
orderIdas the idempotency key - Reconcile reservations, deliveries, and entitlements before closing the incident
The history endpoint helps recover the event. Your own durable records and idempotent transaction determine whether recovery is safe.
Official references
- LINE MINI App API reference
- Implement LINE MINI App in-app purchases
- LINE MINI App in-app purchase development guidelines
Originally published on UnifyPort.
This article was prepared with AI assistance for language and structure, then technically reviewed and verified by the author.
답글 남기기