---
url: https://wiki.awmc.team/en/dev/awmc-api.md
---

# AWMC Gateway Public API (Billing)

For **end users**: how to call open endpoints and **when Tokens are charged**. This page does not cover upstream wire encoding.

::: tip Platform
Platform: https://api.wmc.pub

Docs: https://wiki.awmc.team/en/dev/awmc-api

Sign in with an **AWMC passport (forum account)**, then create a `gw_` token or use the login JWT.
:::

::: warning Auth
Send:

`Authorization: Bearer <token>`

* Session JWT after browser login, or a long-lived **`gw_` token** from the console (keep secret).
  :::

::: tip Buy Tokens
Top up with **card codes** from the [Afdian shop](https://afdian.com/a/AWMC_TEAM?tab=shop).\
Redeem in the console or via `POST /redeem`.
:::

::: info keychip
`keychip` is injected by the gateway. Callers must **not** send it. Provide business fields only (e.g. `qrcode`).
:::

::: danger High-risk operations (read first)
These actions **may cause irreversible, severe damage to an account**. Do not call them casually:

1. **Gate edits**: `POST /v1/kaleidx-scope/upsert` (Kaleidx Scope Gate state).
2. **Unverified collectibles**: writing **`itemKind` `4` / `8` / `15` and other poorly verified collectible types** via `POST /v1/item/upsert` or `POST /v1/user/upsert-all`.

The gateway does **not** block these requests; you accept the risk by calling them.
:::

## 1. Base URL & path compatibility

All business paths are under **`/v1`** on the gateway.

* Upstream is **AWMC API v2 / v2-3 dual mode**; **public paths stay unchanged** (e.g. still `/v1/user/data`).
* The gateway **auto-detects** the upstream generation (~60s cache): `GET /v1/health` responds with `upstreamVersion` (`v2` or `v2-3`) and `sessionApi` (whether writes are session-based).
* **POST** with **plain JSON** (`Content-Type: application/json`). zlib/Base64 is handled by the gateway.
* Responses include:
  * Upstream: `returnCode`, `returnMessage` (and `businessData` when applicable)
  * Compatibility: **`code === 0`** on business success; `msg` often JSON-parseable

**Success (billing)**

| Endpoint | Upstream success |
|----------|------------------|
| `GET /v1/health` | `returnCode === 0` (ping) |
| Other business | `returnCode === 1` |
| Session-based writes (v2-3 upstream) | See [§2.3 billing timing](#_2-3-billing-timing-for-session-based-writes-v2-3-upstream) |

Prefer `returnCode` in new clients; legacy clients may use `code === 0`.

## 2. Token pricing

Charged on **HTTP 2xx** and upstream business success. Suggest **180s** client timeout (200s with `?sync=1`).

**Session-based write endpoints** (all `upsert` ones on a v2-3 upstream): **creation is free**; billing timing in [§2.3](#_2-3-billing-timing-for-session-based-writes-v2-3-upstream). On a legacy v2 upstream they still bill on success.

### 2.1 Legacy-compatible paths

| Method | Path | Cost | Notes |
|--------|------|------|-------|
| GET | `/v1/health` | 0 | Connectivity (ping) |
| POST | `/v1/user/data` | 2 | User data |
| POST | `/v1/user/region` | 2 | Region records |
| POST | `/v1/user/music` | 4 | All scores |
| POST | `/v1/user/charge` | 2 | Owned Charges (read-only) |
| GET | `/v1/charge/queue` | 0 | **Stub**: no real queue; empty `tasks` |
| POST | `/v1/charge` | 10 / 15 / 25 | Buy one Charge (`chargeId`/`charge` = **2 / 3 / 5** → **10 / 15 / 25** Tokens) |
| POST | `/v1/update-lx` | 5 | LXNS sync (`key`+`qrcode`; ignore legacy `type`) |
| POST | `/v1/update-fish` | 5 | Diving-Fish sync |

### 2.2 New paths

#### Basic reads/writes (v2 era)

| Method | Path | Cost | Notes |
|--------|------|------|-------|
| POST | `/v1/user/preview` | 1 | Preview |
| POST | `/v1/user/item-list` | 2 | Items |
| POST | `/v1/user/kaleidx-scope` | 2 | Read Gate state (v2 only; 410 on v2-3, use `/v1/user/item-list`) |
| POST | `/v1/music/upsert` | 15 | Upsert scores (v2: 1–4 sync; v2-3: **1–20, session-based**, see §2.3) |
| POST | `/v1/music/delete` | 10 | Delete scores (v2: 1–4; v2-3: session-based, keeps play counts) |
| POST | `/v1/item/upsert` | 20 | Item write (**high risk**; v2-3 supports **batch `itemList`**, legacy style auto-converted) |
| POST | `/v1/ticket/clear` | 5 | Clear Charges (v2-3: session-based) |
| POST | `/v1/kaleidx-scope/upsert` | 30 | Gate edit (**high risk**; v2-3 maps to `upsert-kaleidx-gate`, `gateId` limited to **1–6**) |
| POST | `/v1/user/upsert-all` | 25 | Combined write (**v2 only**; removed on v2-3 → 410, split into single endpoints) |

#### Session control (v2-3 upstream)

| Method | Path | Cost | Notes |
|--------|------|------|-------|
| POST | `/v1/upsert/continue` | 0 | Confirm an upload session. **Tolerant idempotent**: if already auto-confirmed by the gateway, returns current state without error |
| POST | `/v1/upsert/check` | 0 | Poll session status (idempotent read); `upsert_status=2` **triggers settlement** (see §2.3) |

#### rival sessionless reads (rival-direct patched upstream)

| Method | Path | Cost | Notes |
|--------|------|------|-------|
| POST | `/v1/rival/data` | 2 | Rival profile (player name etc.; no `qrcode` / `keychip` needed, see §3.9) |
| POST | `/v1/rival/music` | 4 | Rival full score summary (per-chart details; no FC/FS fields, see §3.9) |

#### Resource queries (v2-3 upstream; no account params, sync)

| Method | Path | Cost | Notes |
|--------|------|------|-------|
| POST | `/v1/user/ping` | 0 | Game connectivity check (`get-ping`, v2-3 only; useful to detect the generation) |
| POST | `/v1/select/categories` | 1 | Resource categories |
| POST | `/v1/select/resource` | 1 | Paged resources by category (`category` required; `query` / `offset` / `limit` optional) |
| POST | `/v1/select/music` | 1 | Paged songs with chart constants (`charts`, `constant`) |
| POST | `/v1/select/music-unlock-plan` | 1 | Unlock candidates (**resource candidates**, not live account gaps) |

#### Profile writes (v2-3 upstream; all session-based)

| Method | Path | Cost | Notes |
|--------|------|------|-------|
| POST | `/v1/music/upsert-fuzzy` | 15 | Fuzzy score upload (`dxStar` half-star 0–7, applies when `dxScore=0`) |
| POST | `/v1/user/play-count` | 8 | Play count (`playCount` total / `currentPlayCount` current version) |
| POST | `/v1/user/maimile` | 8 | maimile points (0–99999) |
| POST | `/v1/user/map-stock` | 8 | Map stock (0–999) |
| POST | `/v1/user/rating` | 10 | Displayed rating (does not raise the historical best) |
| POST | `/v1/user/course-rank` | 8 | Course rank (0–23, also adds clear records) |
| POST | `/v1/user/class-rank` | 8 | Class rank (0–25, keeps historical best) |
| POST | `/v1/user/chara-slot` | 8 | Character slot (0/1/5 character IDs) |
| POST | `/v1/user/character` | 8 | Character level / awakening / use count |
| POST | `/v1/user/intimate` | 8 | Partner intimacy (`partnerId` = `naviCharaId` from queries) |
| POST | `/v1/user/map-complete` | 15 | Complete maps and grant rewards (**high risk**) |
| POST | `/v1/user/map-challenge` | 15 | Advance map to first unearned challenge song; no progress rollback (**high risk**) |
| POST | `/v1/user/login-bonus` | 8 | Set login bonus to one stamp before claim |
| POST | `/v1/version/reset` | 10 | Reset account to the current game version (does not unlock songs) |

### 2.3 Billing timing for session-based writes (v2-3 upstream)

On a **v2-3** upstream every write endpoint (scores / items / tickets / profile) is a **three-step session**: create → confirm → poll. The gateway does two of them for you:

```
① Create   POST /v1/music/upsert (or other business endpoint)
           → gateway forwards and returns a session (upsert_status=0, 60s confirm window)
② Confirm  the gateway AUTO-calls upsert-continue immediately — no caller action needed
           → the response you get is already "confirmed, uploading" (upsert_status=1)
③ Poll     POST /v1/upsert/check  { "session_id": "..." }     ← caller does this
           → upsert_status: 1 processing / 2 complete / 3 error
```

**Billing rules** (different from legacy v2 "bill on success"):

* **Create and confirm stages are free**;
* Settlement happens **once** when `/v1/upsert/check` returns `upsert_status === 2` (complete), at the creating endpoint's price (idempotent per `session_id`);
* `upsert_status === 3` (upload failure) is **not billed**;
* `/v1/upsert/check` and `/v1/upsert/continue` themselves **cost 0**.

**Session response shape** (`businessData`, returned by create / confirm / poll):

```json
{
  "session_id": "32-char lowercase hex",
  "upsert_kind": "upsert-music-exact",
  "upsert_status": 1,
  "upsert_message": {
    "text": "Uploading in background",
    "phase": "uploading",
    "total_rounds": 2,
    "completed_rounds": 1,
    "estimated_remaining_seconds": 30
  }
}
```

`upsert_message.result` (on completion) contains `verified`, `uploadedMusicCount`, `skippedMusicCount`, `noOp`; on failure read `upsert_message.error`. **Session created ≠ upload complete** — always poll to a terminal state.

One failure case is special: when the terminal `errorCode` is **`5006` / `5007` / `5104`**, the upstream **already wrote the data and only its verification pass didn't finish** (save-verification failure / upload interruption), so the **result may already be persisted** — check the account data (or re-query the same `session_id`) before resending.

**`?sync=1` mode**: append `?sync=1` to any session-based write (e.g. `POST /v1/music/upsert?sync=1`) and the gateway polls on your behalf until the terminal state (default budget 180s) and **returns the final result synchronously** — same semantics as legacy v2, for simple clients that don't want to implement polling.

**410 capability missing**: when an endpoint doesn't exist on the current upstream generation, the gateway returns `410` with `UPSTREAM_CAPABILITY_MISSING` (e.g. `/v1/user/upsert-all`, `/v1/user/game-event`, `/v1/user/kaleidx-scope` on v2-3; or `/v1/select/*`, `/v1/upsert/*` on v2).

**Field whitelist**: the v2-3 upstream strictly validates request fields; extra fields return `1205`. The gateway filters per endpoint (legacy junk like `qr_text`, `type` is stripped automatically) — just send the fields listed on this page.

::: tip Console visualization
Don't want to build your own polling UI? Sign in to the [api.wmc.pub console](https://api.wmc.pub/dashboard.html) — the sidebar "**Upsert Sessions**" page shows session status directly (summary tiles + status badges + per-row live refresh, auto-polling every 5s while any session is processing); see the corresponding API in [§5](#_5-usage-failure-rate).
:::

### Charge / queue behavior change

1. `POST /v1/charge` buys a Charge directly (not enqueue); **10 / 15 / 25** Tokens for `chargeId` **2 / 3 / 5**.
2. `GET /v1/charge/queue` remains but has **no real tasks**.
3. Use `POST /v1/user/charge` to list owned tickets.

## 3. Interactive demos

### 3.1 Health

### 3.2 User reads

### 3.3 Buy Charge & queue stub

### 3.4 Score writes (upload charts) — details

> Do **not** confuse with `POST /v1/user/music` (**read** all scores).\
> **Write/overwrite** → `POST /v1/music/upsert`; **delete** → `POST /v1/music/delete`; **fuzzy** → `POST /v1/music/upsert-fuzzy`.

#### What one request does

* Body: `qrcode` + `musicList` (array).
* `musicList` length: **1–4 on v2**; **1–20 on v2-3** (server auto-splits rounds).
* Each item is uniquely identified by **`(musicId, level)`**.
* Costs **15 Tokens**; **v2**: success when upstream `returnCode === 1` (sync). **v2-3**: session-based (see §2.3) — poll `/v1/upsert/check` afterwards, or add `?sync=1` to wait synchronously.

#### `level`

| Value | Difficulty |
|---:|------|
| 0 | BASIC |
| 1 | ADVANCED |
| 2 | EXPERT |
| 3 | MASTER |
| 4 | Re:MASTER |
| 10 | UTAGE |

#### Exact vs fuzzy — easy to get wrong

**v2 upstream** (legacy contract, `fuzzy` boolean):

| Mode | `fuzzy` | `achievement` | Meaning of `dxScore` |
|------|---------|---------------|----------------------|
| **Exact** | `false` | Target rate, e.g. `100.9444` | **Actual DX score** (e.g. `2947`) |
| **Fuzzy** | `true` | **Minimum** desired rate | **DX star 0–5** (not the raw DX score!) |

**v2-3 upstream** (new contract, split into two endpoints):

| Endpoint | Notes |
|----------|-------|
| `/v1/music/upsert` (→ `upsert-music-exact`) | Exact: `achievement` as protocol integer recommended (`100% = 1000000`; **integer 100 is NOT 100%**); `dxScore` is the raw DX score, must not exceed the chart max |
| `/v1/music/upsert-fuzzy` (→ `upsert-music-fuzzy`) | Fuzzy: `dxStar` **half-star 0–7** (applies when `dxScore=0`); percentages with fewer than 4 decimals get random 1–9 padding |

`dxStar` minimum completion: `0` none · `1` 85% · `1.5` 87.5% · `2` 90% · `2.5` 91.5% · `3` 93% · `3.5` 94% · `4` 95% · `4.5` 96% · `5` 97% · `5.5` 98% · `6` 99% · `6.5` 99.5% · `7` 100%.

::: tip Legacy client compatibility
The v2-era style of `fuzzy: true` inside `musicList` items still works on a v2-3 upstream — the gateway detects it and **reroutes to** `upsert-music-fuzzy` (stripping the flag). The flag must be consistent across the batch, otherwise 400.
:::

#### Combo / Sync enums

* Combo: `none` / `fc` / `fcp` / `ap` / `app` (strings on v2; integers 0–4 on v2-3)
* Sync: `none` / `fs` / `fsp` / `fsd` / `fsdp` / `sync` (strings on v2; integers 0–5 on v2-3)

#### Minimal example (v2-3 exact)

```json
{
  "qrcode": "SGWCMAID...",
  "musicList": [
    {
      "musicId": 11479,
      "level": 3,
      "achievement": 1009444,
      "dxScore": 2100,
      "comboStatus": 4,
      "syncStatus": 0
    }
  ]
}
```

#### Fuzzy example (v2-3, dxStar)

```json
{
  "qrcode": "SGWCMAID...",
  "musicList": [
    { "musicId": 11176, "level": 2, "achievement": 1000000, "dxScore": 0, "dxStar": 3, "comboStatus": 2, "syncStatus": 0 }
  ]
}
```

`musicId` must exist in the current version, otherwise `5004`; `dxScore` over the chart max returns `5009`.

More detail: [API Reference / Swagger](/en/dev/api-docs) → **Score → `/v1/music/upsert`**.

### 3.4b Session polling (v2-3 upstream)

Use the `businessData.session_id` from a session-based write with these two endpoints:

### 3.5 High-risk writes

#### Item write contract (v2-3 upstream)

v2-3 supports **batch `itemList`** (no duplicate `itemKind`/`itemId` in one batch):

```json
{
  "qrcode": "SGWCMAID...",
  "itemList": [
    { "itemKind": 5, "itemId": 10030, "stock": 1, "isValid": true }
  ]
}
```

* `itemKind`: 1 name plate / 2 title / 3 icon / 4 present / 5 music / 6 MASTER / 7 Re:MASTER / 10 partner / 11 frame
* Regular items: `stock=1, isValid=true` to own, `stock=0, isValid=false` to remove
* Presents (`itemKind=4`): `stock` is the **absolute stock** (non-negative integer); `itemId` = source type × 1000000 + resource ID
* If everything is skipped, the upstream returns `noOp=true` and uploads nothing

::: tip Legacy style compatibility
The v2-era single-item style `{ "itemKind": 2, "itemId": 11, "operation": "add" }` (`operation`: `add` / `del`) still works on a v2-3 upstream; the gateway converts it into a one-element `itemList`.
:::

### 3.6 Third-party sync

### 3.7 Resource queries (v2-3 upstream)

No account params (no `qrcode`), sync responses, 1 Token each:

### 3.8 Profile writes (v2-3 upstream, session-based)

Examples: play count / rating. Other endpoints (`maimile`, `character`, `map-*`, …) are listed in [§2.2](#_2-2-new-paths); all session-based (§2.3: create → auto-confirm → poll):

### 3.9 rival sessionless reads (rival-direct patched upstream)

> **Since 2026-10-17** the upstream rival endpoints are verified **sessionless**:
> no QR code (`qrcode`) and no `keychip` — pull rival profile and scores directly
> by **friend code** (= userid). Requires the rival-direct patched upstream
> (stock v2-3 lacks these endpoints; legacy v2 upstream returns 410).
>
> **Official protocol limitation**: rival scores carry **achievement + DX score
> only** — no FC / FS / sync status fields; responses may contain `musicId=0`
> phantom rows and `achievement=0` empty rows, which callers should filter out.

**Field notes**

* `achievement`: 1/10000-scale integer (`997601` = 99.7601%); `deluxscoreMax` is the DX score.
* `level`: `0`–`4` = BASIC–Re:MASTER; Utage is `10` (only returned with `includeUtage: true`).
* `musicId >= 10000` is a DX chart, `< 10000` standard.
* Billed against the **read quota**; upstream failures (e.g. after a WAHLAP key rotation before the EXE baseline is updated) return `SdgbError` with `errorCode` 5110–5113.

**curl example**

```bash
curl -X POST https://api.wmc.pub/v1/rival/music \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{"rivalId": 13225939}'
```

## 4. Public JSON catalog

```http
GET https://api.wmc.pub/api/docs
```

## 5. Usage & failure rate

### 5.1 Usage

| Method | Path | Auth | Scope |
|--------|------|------|-------|
| GET | `/me/usage` | Bearer | Personal log |
| GET | `/me/usage/stats` | Bearer | Daily stats |

### 5.2 Upsert sessions (v2-3 upstream)

Your own upload sessions on a v2-3 upstream (also viewable in the console sidebar page "**Upsert Sessions**" with status badges, live refresh and auto-polling):

| Method | Path | Auth | Scope |
|--------|------|------|-------|
| GET | `/me/upsert-sessions` | Bearer | Own sessions, paginated (`limit` default 50 / `offset` / `status` 0-3); response includes a `summary` (pending/processing/completed/failed/billed) |
| POST | `/me/upsert-sessions/refresh` | Bearer | Body `sessionId`: live upstream check + table sync; idempotent settlement on `upsert_status=2`; upstream errors still return the row + `error` field |

### 5.3 Failure rate

| Method | Path | Auth | Scope |
|--------|------|------|-------|
| GET | `/usage/failure-rate` | None | Site-wide, 7d / 30m buckets |
| GET | `/me/usage/failure-rate` | Bearer | Personal failure rate |

`codeZero` counts business success (`returnCode` 0 for ping, 1 otherwise).

## 6. Common errors

| HTTP / returnCode | Meaning |
|------|---------|
| **401** | Missing/invalid token |
| **403** | Insufficient balance |
| **410** | Endpoint unavailable on the current upstream generation (`UPSTREAM_CAPABILITY_MISSING`, see §2.3) |
| **4001** etc. | Upstream business errors (see `returnMessage`) |
| **5004 / 5009** | Chart not found / DX score over chart max |
| **5101 / 5106** | Session missing or cleaned / not confirmed within 60s (v2-3; rare with gateway auto-confirm) |
| **5110–5113** | rival sessionless direct-call failure (network / non-200 HTTP / response decryption failure / malformed response; decryption failures usually mean a WAHLAP key rotation — update the upstream EXE baseline and re-patch) |
| **5006 / 5007 / 5104** | Upstream save-verification failure / upload interruption (v2-3 session terminal) — the **result may already be persisted**; verify the account data first, don't blindly resend |
| **500 / 502** | Forwarding or decode failure |

::: tip Tips
Start with **`/v1/health`** (check `upstreamVersion` for the generation). Buy tickets with **`/v1/charge`**; do not rely on **`/v1/charge/queue`**.\
On a v2-3 upstream: after a session-based write returns a `session_id`, **always poll** `/v1/upsert/check` until `upsert_status` is 2/3 — forgetting to poll doesn't lose the result (sessions stay queryable), but settlement only happens when a poll sees `2`. Never log QR codes or third-party tokens.
:::
