OrderChatz API
OrderChatz API — companion REST API for OrderChatz. Author: WPBrewer (https://wpbrewer.com/).
by WPBrewer · github.com/oberonlai/order-chatz-api · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/oberonlai/order-chatz-api/archive/refs/heads/main.zipCompanion plugin: REST API for OrderChatz DM conversations (list, detail, reply including media + quote) plus optional outbound webhook for inbound LINE messages. Does not modify OrderChatz core.
- Version: 1.3.1
- Text Domain:
otzapi - Requires: WordPress 6.5+, PHP 8.0+, active OrderChatz (
OTZ_VERSION)
Author: WPBrewer
Ticket: Firstmate FM-93 / Linear WPB-354
Install
- Install and activate OrderChatz
- Upload
order-chatz-apiand activate OrderChatz API - Open OrderChatz → API (or Settings → API) and generate a site token
Auth
Either:
- WordPress Application Password for a user with
manage_options(Basic auth / logged-in REST), or - Site token via:
- Header
X-OTZ-API-Token: <token>, or - Header
Authorization: Bearer <token>
- Header
Token grants access to the endpoints below. Option key: otzapi_site_token (plaintext).
Outbound webhook (inbound LINE → HTTPS POST)
Configure under OrderChatz → API (or Settings → API):
| Setting | Option key | Description |
|---|---|---|
| Enable outbound webhook | otzapi_webhook_enabled |
Checkbox |
| Webhook URL | otzapi_webhook_url |
HTTPS URL only |
| Shared secret (optional) | otzapi_webhook_secret |
Sent as Authorization: Bearer <secret> and X-OTZ-Webhook-Secret when non-empty (paste the raw key only, not the full header line) |
When enabled, the plugin listens to OrderChatz’s otz_line_message_received hook and POSTs JSON for inbound user message events (text + media). Non-message events (follow, postback, …) and non-user sources are skipped.
Request:
- Method:
POST - Headers:
Content-Type: application/json; optionalAuthorization: Bearer <secret>andX-OTZ-Webhook-Secret: <secret> - Transport:
wp_remote_postwith short timeout andblocking => false(async-safe for the LINE webhook path) - Failures: logged via
error_logwith[otzapi] outbound webhook:prefix (never thrown into OrderChatz)
Example payload:
{
"event": "otz.inbound_message",
"line_user_id": "Uxxxx",
"message_type": "text",
"message_text": "Hello",
"message_id": "mid-1",
"quote_token": "qt-1",
"timestamp": 1234567890123,
"source_type": "user",
"raw_event": { }
}
raw_event is omitted when the encoded event exceeds ~32KB.
Endpoints
Namespace: order-chatz/v1
Base URL: https://example.com/wp-json/order-chatz/v1
GET /conversations
List DM friends from {prefix}otz_users (status=active, source_type=user).
| Query | Default | Description |
|---|---|---|
unread |
0 |
1 = only rows with unread inbound messages |
last_from_customer |
0 |
1 = last message sender is customer (user) |
limit |
20 |
Max 100 |
offset |
0 |
Pagination offset |
Response shape:
{
"items": [ { "id": 1, "line_user_id": "...", "unread_count": 2, "last_message": {}, "last_from_customer": true, "...": "..." } ],
"total": 42,
"limit": 20,
"offset": 0
}
GET /conversations/{id}
{id} = otz_users.id. Returns friend, recent DM messages (empty group_id, last 2 month partitions), and up to 5 WooCommerce order summaries when wp_user_id is set and WooCommerce is available.
Each item in messages[] (and list/detail last_message, same normalizer) is read from DB only — no live LINE call. Quote-related fields:
| Field | Type | Description |
|---|---|---|
line_message_id |
string|null | LINE message ID for this row |
quote_token |
string|null | Token to pass on POST when quoting this message |
quoted_message_id |
string|null | If this message was itself a quote reply, parent LINE message id |
Missing DB values are null (never invented).
Example message object:
{
"line_user_id": "Uxxxx",
"sender_type": "user",
"group_id": null,
"sent_date": "2026-09-10",
"sent_time": "08:00:00",
"sent_at": "2026-09-10 08:00:00",
"message_type": "text",
"message_content": "Hello",
"sender_name": "Customer",
"line_message_id": "mid-1",
"quote_token": "qt-1",
"quoted_message_id": null
}
POST /conversations/{id}/messages
Send an outbound reply (text or media). 留言/引用回覆 = quote reply via quote_token + quoted_message_id (there is no separate LINE comment table; customer「備註」is private notes and is not this API).
Shared body fields:
| Field | Required | Description |
|---|---|---|
type |
no | text (default) | image | video | file | sticker |
quote_token |
no | LINE quote token (引用) |
quoted_message_id |
no | Quoted LINE message id (引用) |
Per-type fields:
| type | Required fields |
|---|---|
text |
message or text (max 5000) |
image |
image_url (HTTPS) |
video |
video_url (HTTPS), video_name — LINE gets a text summary with watch link (mirrors OrderChatz AJAX) |
file |
file_url (HTTPS), file_name — LINE gets a text summary with download link |
sticker |
package_id, sticker_id |
Auth: same as GET (Application Password manage_options or site token).
Behavior:
- Prefer OrderChatz public helpers when loaded:
MessageQueryService::getLatestReplyToken→ reply API, else push; reply failure falls back to push- Image:
sendReplyImageMessage/sendPushImageMessage+saveImageMessage - File/video: text summary via
sendReplyMessage/sendPushMessage+saveFileMessage/saveVideoMessage - Sticker:
sendReplyStickerMessage/sendPushStickerMessage+saveStickerMessage - Text:
saveOutboundMessage
- Text-only fallback (no OrderChatz classes):
otz_access_tokenLINE push + monthlyotz_messages_YYYY_MMwrite. - Media requires OrderChatz collaborators (
501if unavailable). v1 accepts HTTPS URLs only — no multipart upload.
Success: 201
{
"success": true,
"api_used": "push",
"line_message_id": "...",
"quote_token": "...",
"conversation_id": 123,
"message": {
"line_user_id": "...",
"sender_type": "ACCOUNT",
"message_type": "image",
"message_content": "https://cdn.example.com/a.jpg",
"sender_name": "..."
}
}
Errors: 400 validation / unknown type, 401/403 auth, 404 unknown conversation, 501 media without OrderChatz, 502 LINE send failure.
Example curls
Replace SITE, credentials, and token.
Application Password
# List conversations
curl -sS -u 'admin:APPLICATION_PASSWORD' \
'https://SITE/wp-json/order-chatz/v1/conversations?limit=20'
# Detail
curl -sS -u 'admin:APPLICATION_PASSWORD' \
'https://SITE/wp-json/order-chatz/v1/conversations/123'
# Text reply
curl -sS -u 'admin:APPLICATION_PASSWORD' \
-H 'Content-Type: application/json' \
-d '{"message":"Thanks for your order!"}' \
'https://SITE/wp-json/order-chatz/v1/conversations/123/messages'
# Quote (留言/引用) reply
curl -sS -u 'admin:APPLICATION_PASSWORD' \
-H 'Content-Type: application/json' \
-d '{"type":"text","message":"Got it","quote_token":"QUOTE_TOKEN","quoted_message_id":"LINE_MID"}' \
'https://SITE/wp-json/order-chatz/v1/conversations/123/messages'
# Image
curl -sS -u 'admin:APPLICATION_PASSWORD' \
-H 'Content-Type: application/json' \
-d '{"type":"image","image_url":"https://cdn.example.com/a.jpg"}' \
'https://SITE/wp-json/order-chatz/v1/conversations/123/messages'
# File
curl -sS -u 'admin:APPLICATION_PASSWORD' \
-H 'Content-Type: application/json' \
-d '{"type":"file","file_url":"https://cdn.example.com/doc.pdf","file_name":"doc.pdf"}' \
'https://SITE/wp-json/order-chatz/v1/conversations/123/messages'
Site token (X-OTZ-API-Token)
curl -sS -H 'X-OTZ-API-Token: YOUR_SITE_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"message":"Hello from API"}' \
'https://SITE/wp-json/order-chatz/v1/conversations/123/messages'
Site token (Bearer)
curl -sS -H 'Authorization: Bearer YOUR_SITE_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"message":"Hello"}' \
'https://SITE/wp-json/order-chatz/v1/conversations/123/messages'
Unread / last message rules
- Unread: inbound (
sender_type=user, case-insensitive), emptygroup_id,CONCAT(sent_date,' ',sent_time) > read_time. Ifread_timeis empty, cutoff isNOW() - 3 hours. Scans last 2 month tables{prefix}otz_messages_YYYY_MM. - Last message: latest DM (empty group) for
line_user_id. - last_from_customer: last message
sender_typeisuser.
Development
composer install
composer test:install # drops/recreates wordpress_test
composer test
composer phpstan
composer phpcs
Assumptions / blockers
- Reply prefers OrderChatz send stack; text fallback needs
otz_access_tokenand an existing monthly messages table. - Media is URL-based only in 1.2.0 (no multipart upload endpoint).
- 留言回覆 = quote reply only (not 備註).
License
GPL-2.0+
Changelog
1.3.0
- Outbound webhook: optional HTTPS POST on
otz_line_message_received(settings on OrderChatz → API). - Header
X-OTZ-Webhook-Secretfor optional shared secret; JSON eventotz.inbound_message.
1.2.1
- GET
messages[]/last_messageexposeline_message_id,quote_token,quoted_message_idfrom DB.
1.2.0
- Media reply types:
image,video,file,stickeron POST/conversations/{id}/messages. - Quote / 留言回覆 via
quote_token+quoted_message_idon text and media. - Document URL-only media; OrderChatz required for media sends.
1.1.0
- POST
/conversations/{id}/messagesoutbound text reply via OrderChatz helpers (fallback LINE push + DB write). - Dev tooling: PHPUnit, PHPStan, PHPCS, coverage gate, build script.
1.0.1
- REST namespace:
order-chatz/v1(wasorder-chatz-api/v1).
1.0.0
- Initial read-only conversations API.