npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@msgly/wechat

v0.9.0

Published

WeChat Official Account adapter for Msgly

Readme

@msgly/wechat

WeChat Official Account adapter for Msgly. Send and receive WeChat messages through the unified hub — text, image, video, voice, location, menu interactions. Zero classes, runs in Node and Next.js.

Install

npm install @msgly/core @msgly/wechat

Quick start

import express from 'express';
import { createHub } from '@msgly/core';
import { createWeChatAdapter } from '@msgly/wechat';

const hub = createHub();

hub.register(
  createWeChatAdapter({
    appId: process.env.WECHAT_APP_ID!,
    appSecret: process.env.WECHAT_APP_SECRET!,
    token: process.env.WECHAT_TOKEN!,
  }),
);

await hub.connect({ throwOnFailure: true });

hub.on('message', async (msg) => {
  if (msg.channel === 'wechat' && msg.content.type === 'text') {
    await hub.send({
      channel: 'wechat',
      account: msg.account,
      contact: msg.contact,
      content: { type: 'text', text: `You said: ${msg.content.text}` },
    });
  }
});

const app = express();
// WeChat sends XML — capture rawBody as bytes
app.use(express.raw({ type: '*/*', verify: (req, _r, buf) => ((req as any).rawBody = new Uint8Array(buf)) }));

const handlers = hub.createWebhookHandler();
// WeChat uses GET for challenge verification and POST for messages
app.get('/webhook/:channel', handlers.get);
app.post('/webhook/:channel', handlers.post);

app.listen(3000);

Config

interface WeChatConfig {
  /** WeChat Official Account App ID (mp.weixin.qq.com → Development → Basic Configuration). */
  appId: string;

  /** WeChat Official Account App Secret. */
  appSecret: string;

  /**
   * Token you set in Development → Basic Configuration → Token.
   * Used to verify that webhook calls are genuine WeChat requests.
   */
  token: string;

  /** Override for tests. Defaults to https://api.weixin.qq.com. */
  apiBase?: string;
}

Setup (15 minutes)

Prerequisites: A WeChat Official Account (Service Account or Subscription Account). Service Accounts get more API quota and are required for the Customer Service message API.

  1. Open mp.weixin.qq.comSettings → Official Account Settings → Account Details and note your App ID.
  2. Go to Development → Basic Configuration:
    • Enable developer mode (启用).
    • Set Token to any random string — save it as WECHAT_TOKEN.
    • Set Server Address (URL) to <PUBLIC_URL>/webhook/wechat.
    • Submit — WeChat sends a GET request with a challenge to verify the URL.
  3. Copy AppID and AppSecret from the same page.
  4. Set environment variables:
    WECHAT_APP_ID=wx...
    WECHAT_APP_SECRET=...
    WECHAT_TOKEN=your-random-token
  5. For the Customer Service Message API (sending replies), go to Development → API Permissions and ensure 客服消息 (Customer Service Message) is enabled.

Capabilities

| Feature | Supported | | ------------- | --------- | | text | ✓ | | image | ✓ | | video | ✓ | | audio (voice) | ✓ | | file | — | | location | ✓ (recv) / text (send) | | buttons | — | | quick replies | ✓ (msgmenu) | | reactions | — | | typing | — | | templates | — |

Access token management

WeChat access tokens expire every 2 hours. The adapter caches the token in memory and refreshes it automatically with a 60-second buffer. For multi-process deployments (PM2 cluster, serverless), each process caches independently — if you need a shared token store, call adapter.getAccessToken() and manage caching yourself:

const adapter = createWeChatAdapter(config);
const token = await adapter.getAccessToken();
// Use `token` directly with the WeChat Graph API

Sending examples

Text

await hub.send({
  channel: 'wechat',
  account, contact,
  content: { type: 'text', text: 'Hello!' },
});

Image

WeChat requires uploading media first to get a media_id:

import { readFileSync } from 'fs';

const adapter = hub.getAdapter('wechat') as WeChatAdapter;
const ref = await adapter.uploadMedia({
  data: readFileSync('./banner.jpg'),
  mimeType: 'image/jpeg',
  filename: 'banner.jpg',
});

await hub.send({
  channel: 'wechat',
  account, contact,
  content: { type: 'image', mediaRef: ref },
});

Note: Temporary media IDs from uploadMedia expire after 3 days.

Quick reply menu (msgmenu)

await hub.send({
  channel: 'wechat',
  account, contact,
  content: {
    type: 'interactive',
    text: 'How can I help you?',
    buttons: [
      { id: 'track', label: 'Track my order' },
      { id: 'return', label: 'Return an item' },
      { id: 'faq', label: 'FAQ' },
    ],
  },
});

When the user taps an option, your message handler receives an InboundMessage with interaction.id equal to the button's id:

hub.on('message', async (msg) => {
  if (msg.interaction) {
    console.log('user tapped:', msg.interaction.id);
  }
});

Maximum 5 items per menu (3 for Subscription Accounts).

Formatting

WeChat DMs are plain text. The fmt export is provided so code that imports fmt from any adapter compiles uniformly:

import { fmt } from '@msgly/wechat';
fmt.bold('Hello')  // → 'Hello' (pass-through)

Webhook body parsing

WeChat sends messages as XML (not JSON). Your Express middleware must not parse the body before the adapter sees it. Use express.raw() to capture raw bytes:

// ✓ Correct — raw bytes preserved
app.use(express.raw({ type: '*/*', verify: (req, _r, buf) => ((req as any).rawBody = new Uint8Array(buf)) }));

// ✗ Wrong — express.json() will fail to parse XML and throw before the adapter runs
app.use(express.json());

Common pitfalls

  • GET challenge fails: the token in config must exactly match the Token field in the WeChat console. A single character difference causes a signature mismatch.
  • "System busy" (errcode: -1): usually a rate-limit or token issue. Check that appId and appSecret are correct and the account is in developer mode.
  • Messages arrive but no reply sent: the Customer Service Message API (客服消息) requires the user to have sent a message in the past 48 hours. Outside this window, use template messages (requires additional approval from WeChat).
  • Media sends fail: temporary media IDs expire after 3 days. Re-upload if you store IDs long-term.
  • Multi-process token contention: each Node process maintains its own token cache. Under high concurrency you may get brief duplicate refresh calls — WeChat handles this gracefully, but if token quota is a concern, centralize token storage in Redis.

Documentation

Full setup walkthrough and multi-channel usage: https://github.com/AyushJain070401/msgly

License

MIT