Managing Audience Members via Public API
Overview
The /api/audience endpoints form a public API intended for external systems to call directly —
not for LINE and not for the CMS. Customers and partners holding an api-key can:
- List the audiences they own
- Add or remove LINE users from an audience, up to 10,000 IDs per call
- Clear all members from an audience
The primary consumer visible in the code is a system called mookept (the mookept_audience
queue and the audience template slugs mookept-1 through mookept-4) — a B2B integration that
pushes customer lists into our platform for campaign targeting.
Every endpoint in this group requires three-header api-key authentication (see
API Key Authentication) and is always scoped by the caller's api_key
row (lineOaId, organizationId, apiKeyId), so a caller can only see and modify its own data.
Business Flow
1. GET /api/audience — list the caller's audiences
- The middleware authenticates the client and stores
apiClientIdandapiKeyin the context. - Resolve the scope with
SELECT ... FROM api_key WHERE api_client_id=$1 AND key=$2 AND status='active'. - If no row matches, respond
200withnull— parity with the NestJS original, which returnedundefined. - Otherwise, fetch the list with
SELECT id, title, slug FROM audience WHERE api_client_id=$1 AND api_key_id=$2.
2. PUT /api/audience/:id — add or remove members asynchronously
- Parse
:idas anint64; on failure respond400 updatememberbyaudienceiddto::id::invalid. - Validate the DTO against the original class-validator rules:
lineUserIds— a non-empty array of strings, each matching^U[0-9a-f]{32}$(case-insensitive), with a length between 1 and 10,000.actionType— lowercased first, then required to be eitheraddorremove.isComplete— a required boolean.lineChannelId— required.
- Resolve the scope from
api_key. If no row is found the response is500, not404, because the original code dereferenced the value directly. - Publish bare JSON to the
mookept_audiencequeue in the shape{lineUserIds, actionType, isComplete, lineChannelId, audienceId, organizationId, lineOaId}. - Wait for the publish to succeed, then echo the request back as
{fn:"updateMemberByAudienceId", apiClientId, apiKey, id, body}.
The real work — writing the member list into the audience CSV and its related tables — is carried out by worker-go.
3. DELETE /api/audience/:id — clear all members synchronously
- Parse
:id, then loosely read the body to extractisComplete. The original had no DTO here, so an empty body yieldsfalse. - Step 1 — if no
api_keyrow is found, respond400 "Step 1: Checking line oa by api key". - Step 2 — look up the audience with
SELECT ... WHERE id=$1 AND line_oa_id=$2 AND organization_id=$3 AND api_client_id=$4- Not found responds
400 "... Audience id (N) not found". - A
statusofprocessingresponds400 "... Audience id (N) still processing". (The error strings include the emoji carried over from the original.)
- Not found responds
- Reset the record —
UPDATE audience SET info=(default), status=$2, line_oa_id, organization_id, api_client_id, updated_by=0, updated_date=now WHERE id=$8— wherestatusbecomescompletedwhenisCompleteis true andprocessingotherwise. - Respond with
{code:"RES_SUCCESS_003", message:"Delete successful"}.
4. Audience templates (invoked from another module)
CreateAudienceTemplate is exported so that LINE OA Verification can call
it. It seeds four starter audiences for a new customer — slugs mookept-1 through mookept-4,
namely New member, First purchase, Repeat buyer, and Most loyalty customer. Creation is idempotent
per slug; when a title collides, -{epochMs} is appended before inserting with
data_source='filter', status='completed', details='', and created_by=0.
Key Files & Functions
| Method | Route | Auth | Handler |
|---|---|---|---|
| GET | /api/audience | api-key | Handler.getListByApiKey |
| PUT | /api/audience/:id | api-key | Handler.updateMemberByAudienceId |
| DELETE | /api/audience/:id | api-key | Handler.deleteAllMembersByAudienceId |
All source lives under internal/audience/.
| File | Highlights |
|---|---|
handler.go | Register (wrapped in deps.APIKeyAuth()), caller, parseID, and the three handlers |
service.go | Service.GetListByApiKey, .UpdateMemberByAudienceId, .DeleteAllMembersByAudienceId, .CreateAudienceTemplate, .resolveScope |
repository.go | FindActiveBySlug, FindActiveByTitle, FindByIDScoped, ListByClientAndKey, Insert, UpdateMembersReset |
entity.go | struct Audience, ListItem, AudienceInfo (jsonb Scanner/Valuer), status constants, systemUser = 0 |
dto.go | UpdateMemberByAudienceIdDto and its Rules() method — a port of the class-validator schema |
Connections to Other Services
- Postgres — the
audiencetable (read and write), plusapi_keyfor scoping andapi_clientfor authentication, accessed through sqlx over pgx using the simple query protocol. See Core HTTP & Database Access. - RabbitMQ — the
mookept_audiencequeue (envRABBITMQ_QUEUE_MOOKEPT_AUDIENCE_QUEUE); see RabbitMQ Publisher & Topology. - worker-go — consumes
mookept_audience, performs the actual member add/remove against the audience CSV file, and updatesaudience.info.stats. - cms-api — the
audience-managementandaudience-filtermodules manage the same audiences from the CMS side, whileapi-clientandapi-keyissue the caller's credentials. - Related database domains:
audienceandapi-client-key.
:::caution JSONB contract
The audience.info column must preserve its field names exactly
({originalFile, lineUserIdsPathFilename, stats:{totalOriginal,totalSuccess,totalFail}}), because
the document is shared between webhook-go, cms-api, and worker-go.
:::