背景
前一段模仿WhatsApp Business的核心功能做了一套商业号服务,为了让老外看起来亲切,因此其接口设计尽量与WhatsApp保持一致,因此可以将此接口设计share出来,通过该接口设计,大家可以了解到WhatsApp Business的核心功能及其对应的接口定义;
尽管如此,一些地方的设计也有小差异,例如:WhatsApp直接使用手机号码定位用户,在这套设计中则借鉴了微信公众号OpenUserId的套路,另外为了安全考虑对真实userId又进行了加入应用ID因子的混淆,因此可以做到:同一个用户不同应用(订阅号)下的OpenUserId不同,OpenUserId实现参考如下:
- ByteUtil
package codec;
import org.apache.commons.lang3.ArrayUtils;
/**
* ByteUtil
*
* @author chenx
*/
public class ByteUtil {
public static final int ONE_BYTE_INT_MAX_VALUE = 127;
public static final int ONE_BYTE_INT_MIN_VALUE = -128;
public static final int TIMESTAMP_BYTES = 6;
private static final String INPUT_BYTES_LENGTH_MUST_MORE_THAN = "The input bytes.length must >=";
private static final String INPUT_BYTES_LENGTH_MUST_BE = "The input bytes.length must be ";
private ByteUtil() {
}
/**
* timestampToSixBytes
*
* @param timestamp
* @return
*/
public static byte[] timestampToSixBytes(long timestamp) {
byte[] data = new byte[TIMESTAMP_BYTES];
data[0] = (byte) (timestamp >>> 40);
data[1] = (byte) (timestamp >>> 32);
data[2] = (byte) (timestamp >>> 24);
data[3] = (byte) (timestamp >>> 16);
data[4] = (byte) (timestamp >>> 8);
data[5] = (byte) (timestamp >>> 0);
return data;
}
/**
* sixBytesToTimestamp
*
* @param bytes
* @return
*/
public static long sixBytesToTimestamp(byte[] bytes) {
if (ArrayUtils.isEmpty(bytes) || bytes.length < TIMESTAMP_BYTES) {
throw new IllegalArgumentException(INPUT_BYTES_LENGTH_MUST_MORE_THAN + TIMESTAMP_BYTES);
}
return (((long) (bytes[0] & 255) << 40) +
((long) (bytes[1] & 255) << 32) +
((long) (bytes[2] & 255) << 24) +
((bytes[3] & 255) << 16) +
((bytes[4] & 255) << 8) +
((bytes[5] & 255) << 0));
}
/**
* short2Bytes
*
* @param num
* @return
*/
public static byte[] short2Bytes(short num) {
byte[] bytes = new byte[Short.BYTES];
bytes[0] = (byte) (num >> 8);
bytes[1] = (byte) num;
return bytes;
}
/**
* bytes2Short
*
* @param bytes
* @return
*/
public static short bytes2Short(byte[] bytes) {
if (ArrayUtils.isEmpty(bytes) || bytes.length != Short.BYTES) {
throw new IllegalArgumentException(INPUT_BYTES_LENGTH_MUST_BE + Short.BYTES);
}
return (short) ((bytes[0] << 8) | (bytes[1] & 0xFF));
}
/**
* int2Bytes
*
* @param num
* @return
*/
public static byte[] int2Bytes(int num) {
byte[] byteNum = new byte[Integer.BYTES];
for (int ix = 0; ix < Integer.BYTES; ++ix) {
int offset = 32 - (ix + 1) * 8;
byteNum[ix] = (byte) ((num >> offset) & 0xff);
}
return byteNum;
}
/**
* bytes2Int
*
* @param bytes
* @return
*/
public static int bytes2Int(byte[] bytes) {
if (ArrayUtils.isEmpty(bytes) || bytes.length != Integer.BYTES) {
throw new IllegalArgumentException(INPUT_BYTES_LENGTH_MUST_BE + Integer.BYTES);
}
int num = 0;
for (int ix = 0; ix < Integer.BYTES; ++ix) {
num <<= 8;
num |= (bytes[ix] & 0xff);
}
return num;
}
/**
* long2Bytes
*
* @param num
* @return
*/
public static byte[] long2Bytes(long num) {
byte[] byteNum = new byte[Long.BYTES];
for (int ix = 0; ix < Long.BYTES; ++ix) {
int offset = 64 - (ix + 1) * 8;
byteNum[ix] = (byte) ((num >> offset) & 0xff);
}
return byteNum;
}
/**
* bytes2Long
*
* @param bytes
* @return
*/
public static long bytes2Long(byte[] bytes) {
if (ArrayUtils.isEmpty(bytes) || bytes.length != Long.BYTES) {
throw new IllegalArgumentException(INPUT_BYTES_LENGTH_MUST_BE + Long.BYTES);
}
long num = 0;
for (int ix = 0; ix < Long.BYTES; ++ix) {
num <<= 8;
num |= (bytes[ix] & 0xff);
}
return num;
}
/**
* mergeBytes
*
* @param arrays
* @return
*/
public static byte[] mergeBytes(byte[]... arrays) {
int totalLength = 0;
for (byte[] array : arrays) {
totalLength += array.length;
}
byte[] result = new byte[totalLength];
int currentIndex = 0;
for (byte[] array : arrays) {
System.arraycopy(array, 0, result, currentIndex, array.length);
currentIndex += array.length;
}
return result;
}
/**
* getBytesFromStart
*
* @param array
* @param length
* @return
*/
public static byte[] getBytesFromStart(byte[] array, int length) {
if (ArrayUtils.isEmpty(array)) {
throw new IllegalArgumentException("the input bytes is empty!");
}
if (length <= 0) {
throw new IllegalArgumentException("The length must >0!");
}
if (array.length < length) {
throw new IllegalArgumentException("the input bytes.length must > " + length + "!");
}
byte[] result = new byte[length];
System.arraycopy(array, 0, result, 0, length);
return result;
}
/**
* obfuscateBytes
*
* @param bytes
* @param hashBytesLength
*/
public static void hashObfuscate(byte[] bytes, int hashBytesLength) {
if (hashBytesLength <= 0) {
throw new IllegalArgumentException("invalid hashLength!");
}
if (ArrayUtils.isEmpty(bytes) || bytes.length <= hashBytesLength) {
throw new IllegalArgumentException("bytes.length <= " + hashBytesLength);
}
int leftBytesSize = bytes.length - hashBytesLength;
byte[] hashBytes = new byte[hashBytesLength];
byte[] leftBytes = new byte[leftBytesSize];
System.arraycopy(bytes, 0, hashBytes, 0, hashBytesLength);
System.arraycopy(bytes, hashBytesLength, leftBytes, 0, leftBytesSize);
for (int i = hashBytesLength; i < bytes.length; i++) {
bytes[i] = (byte) (hashBytes[i % hashBytesLength] ^ leftBytes[i - hashBytesLength]);
}
}
}
- UUIDUtil
package codec;
import org.apache.commons.lang3.ArrayUtils;
import java.io.*;
import java.util.Base64;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/**
* UUIDUtil
*
* @author chenx
*/
public class UUIDUtil {
public static final int UUID_BYTES = Long.BYTES + Long.BYTES;
private UUIDUtil() {
// just do nothing
}
/**
* getUUID
*/
public static UUID getUUID() {
ThreadLocalRandom random = ThreadLocalRandom.current();
return new UUID(random.nextLong(), random.nextLong());
}
/**
* getUUID
*
* @param msb
* @param lsb
* @return
*/
public static UUID getUUID(long msb, long lsb) {
return new UUID(msb, lsb);
}
/**
* encode
*
* @param uuid
* @return
*/
public static String encode(UUID uuid) {
byte[] data = serialize(uuid);
return Base64.getUrlEncoder().withoutPadding().encodeToString(data);
}
/**
* decode
*
* @param input
* @return
*/
public static UUID decode(String input) {
byte[] data = Base64.getUrlDecoder().decode(input);
return deserialize(data);
}
/**
* serialize
*
* @param uuid
* @return
*/
public static byte[] serialize(UUID uuid) {
if (uuid == null) {
throw new RuntimeException("uuid is null!");
}
long msb = uuid.getMostSignificantBits();
long lsb = uuid.getLeastSignificantBits();
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(out)
) {
dos.writeLong(msb);
dos.writeLong(lsb);
return out.toByteArray();
} catch (IOException ex) {
throw new RuntimeException("UUIDUtil.serialize() failed!");
}
}
/**
* deserialize
*
* @param data
* @return
*/
public static UUID deserialize(byte[] data) {
if (ArrayUtils.isEmpty(data)) {
throw new RuntimeException("data is null!");
}
if (data.length != UUID_BYTES) {
throw new IllegalArgumentException("Invalid data!");
}
try (ByteArrayInputStream in = new ByteArrayInputStream(data);
DataInputStream dis = new DataInputStream(in)
) {
long msb = dis.readLong();
long lsb = dis.readLong();
return getUUID(msb, lsb);
} catch (IOException ex) {
throw new RuntimeException("UUIDUtil.deserialize() failed!");
}
}
}
- OpenUserId
package codec;
/**
* OpenUserId
*
* @author chenx
*/
public class OpenUserId {
private String applicationId;
private String uid;
public OpenUserId() {
}
public OpenUserId(String applicationId, String uid) {
this.applicationId = applicationId;
this.uid = uid;
}
public String getApplicationId() {
return this.applicationId;
}
public void setApplicationId(String applicationId) {
this.applicationId = applicationId;
}
public String getUid() {
return this.uid;
}
public void setUid(String uid) {
this.uid = uid;
}
@Override
public String toString() {
return "OpenUserId{" +
"applicationId='" + this.applicationId + '\'' +
", uid='" + this.uid + '\'' +
'}';
}
}
- OpenUserIdCodec
package codec;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static codec.UUIDUtil.UUID_BYTES;
/**
* OpenUserIdCodec
*
* @author chenx
*/
public class OpenUserIdCodec {
private static final char[] DIGITS64 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_".toCharArray();
private static final Map<Character, Integer> DIGITS64_MAP = createDigits64Map();
private static final int UID_STRING_LENGTH = 22;
private OpenUserIdCodec() {
}
/**
* getUserId
*
* @return
*/
public static String getUserId() {
return getUserId(UUIDUtil.getUUID());
}
/**
* getUserId
*
* @param uuid
* @return
*/
public static String getUserId(UUID uuid) {
return toIDString(uuid.getMostSignificantBits()) + toIDString(uuid.getLeastSignificantBits());
}
/**
* getApplicationId
*
* @return
*/
public static String getApplicationId() {
return UUIDUtil.encode(UUIDUtil.getUUID());
}
/**
* getApplicationId
*
* @param data
* @return
*/
public static String getApplicationId(byte[] data) {
UUID uuid = UUIDUtil.deserialize(data);
return UUIDUtil.encode(uuid);
}
/**
* encode
*
* @param applicationId
* @param uid
* @return
*/
public static String encode(String applicationId, String uid) {
byte[] data = serialize(applicationId, uid);
ByteUtil.hashObfuscate(data, Long.BYTES);
return Base64.getUrlEncoder().withoutPadding().encodeToString(data);
}
/**
* decode
*
* @param openUserId
* @return
*/
public static OpenUserId decode(String openUserId) {
if (StringUtils.isEmpty(openUserId)) {
throw new RuntimeException("The input openUserId is empty!");
}
byte[] data = Base64.getUrlDecoder().decode(openUserId);
ByteUtil.hashObfuscate(data, Long.BYTES);
return deserialize(data);
}
/**
* parseUserId
*
* @param uid
* @return
*/
public static UUID parseUserId(String uid) {
if (StringUtils.isEmpty(uid)) {
throw new RuntimeException("uid is empty!");
}
if (uid.length() != UID_STRING_LENGTH) {
throw new RuntimeException("Invalid uid!");
}
int halfLength = UID_STRING_LENGTH / 2;
String msbStr = uid.substring(0, halfLength);
String lsbStr = uid.substring(halfLength, UID_STRING_LENGTH);
long msb = fromIDString(msbStr);
long lsb = fromIDString(lsbStr);
return new UUID(msb, lsb);
}
/**
* serialize
*
* @param applicationId
* @param uid
* @return
*/
private static byte[] serialize(String applicationId, String uid) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream os = new DataOutputStream(out)
) {
byte[] uidBytes = UUIDUtil.serialize(parseUserId(uid));
byte[] applicationIdBytes = UUIDUtil.serialize(UUIDUtil.decode(applicationId));
os.write(uidBytes);
os.write(applicationIdBytes);
return out.toByteArray();
} catch (Exception ex) {
throw new RuntimeException("OpenUserIdCodec.serialize() failed!");
}
}
/**
* deserialize
*
* @param data
* @return
*/
private static OpenUserId deserialize(byte[] data) {
if (ArrayUtils.isEmpty(data)) {
throw new RuntimeException("The input data is empty!");
}
try (ByteArrayInputStream in = new ByteArrayInputStream(data);
DataInputStream is = new DataInputStream(in)
) {
byte[] uidBytes = new byte[UUID_BYTES];
byte[] applicationIdBytes = new byte[UUID_BYTES];
if (is.read(uidBytes) != UUID_BYTES) {
throw new RuntimeException("OpenUserIdCodec.deserialize() failed!(uidBytes read failed)");
}
if (is.read(applicationIdBytes) != UUID_BYTES) {
throw new RuntimeException("OpenUserIdCodec.deserialize() failed!(applicationIdBytes read failed)");
}
String uid = getUserId(UUIDUtil.deserialize(uidBytes));
String applicationId = getApplicationId(applicationIdBytes);
return new OpenUserId(applicationId, uid);
} catch (Exception ex) {
throw new RuntimeException("OpenUserIdCodec.deserialize() failed!");
}
}
/**
* toIDString
*
* @param num
* @return
*/
private static String toIDString(long num) {
char[] buf = "00000000000".toCharArray();
int length = 11;
long least = 63L;
do {
--length;
buf[length] = DIGITS64[(int) (num & least)];
num >>>= 6;
} while (num != 0L);
return new String(buf);
}
/**
* fromIDString
*
* @param idString
* @return
*/
private static long fromIDString(String idString) {
long result = 0L;
char[] chars = idString.toCharArray();
for (char c : chars) {
result <<= 6;
if (!DIGITS64_MAP.containsKey(c)) {
throw new IllegalArgumentException("Invalid character: " + c);
}
int index = DIGITS64_MAP.get(c);
result |= index;
}
return result;
}
/**
* createDigits64Map
*
* @return
*/
private static Map<Character, Integer> createDigits64Map() {
Map<Character, Integer> map = new HashMap<>(DIGITS64.length);
for (int i = 0; i < DIGITS64.length; i++) {
map.put(DIGITS64[i], i);
}
return map;
}
}
- 测试代码:
接口设计主要分三部分:消息发送接口、媒体接口;Webhook接口,其中核心的消息发送接口概括如下:
- 普通消息:文本消息,媒体消息,位置消息,验证码消息
- 互动消息:回复按钮消息、回复列表消息;
- 模板消息;
备注:
1、验证码消息WhatsApp没有提供而是由提供商去实现,这里我们直接以OpenAPI的方式对外提供。
2、媒体消息后来我们扩展了支持媒体+文字的形式,例如可以支持:图文消息、视频文字消息等。
1. API Authorization
The XXX Official Account API authenticates using an ACCESS_TOKEN, which is maintained by developers in the developer admin portal. An example is provided as the below:
POST <HTTP_URL>
Authorization: <ACCESS_TOKEN>
Content-Type: application/json
{
<PAYLOAD_CONTENT>
}
2. Message
2.1 Send Message
2.1.1 Normal Message
2.1.1.1 Text Message
Example
POST {domain}/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/message
Authorization: ACCESS_TOKEN
Content-Type: application/json
{
"to": "<RECEIVER_USER_ID>",
"type": "text",
"text": {
"body": "<MESSAGE_CONTENT>"
}
}
Definition
Name | Type | Required | Description |
---|---|---|---|
to | String | Y | RECEIVER_USER_ID |
type | String | Y | In this case, type is text |
text | Object | Y | |
body | String | Y | MESSAGE_CONTENT, Maximum length: 4096 characters. |
2.1.1.2 Media Message
Example
POST {domain}/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/message
Authorization: ACCESS_TOKEN
Content-Type: application/json
{
"to": "<RECEIVER_USER_ID>",
"type": "media",
"media": {
"id" : "<MEDIA_OBJECT_ID>"
}
}
Definition
Name | Type | Required | Description |
---|---|---|---|
to | String | Y | RECEIVER_USER_ID |
type | String | Y | In this case, type is media |
media | Object | Y | |
id | String | Y | MEDIA_OBJECT_ID |
2.1.1.3 Location Message
Example
POST {domain}/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/message
Authorization: ACCESS_TOKEN
Content-Type: application/json
{
"to": "<RECEIVER_USER_ID>",
"type": "location",
"location": {
"longitude": <LONG_NUMBER>,
"latitude": <LAT_NUMBER>,
"name": <LOCATION_NAME>,
"address": <LOCATION_ADDRESS>
}
}
Definition
Name | Type | Required | Description |
---|---|---|---|
to | String | Y | RECEIVER_USER_ID |
type | String | Y | In this case, type is location |
location | Object | Y | |
longitude | Number | Y | LONG_NUMBER |
latitude | Number | Y | LAT_NUMBER |
name | String | Y | LOCATION_NAME |
address | String | Y | LOCATION_ADDRESS |
2.1.1.4 Reply To Message
Example
POST {domain}/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/message
Authorization: ACCESS_TOKEN
Content-Type: application/json
{
"to": "<RECEIVER_USER_ID>",
"type": "text",
"text": {
"body": "<MESSAGE_CONTENT>"
},
"context": {
"message_id": "<MESSAGE_ID>"
}
}
Definition
Name | Type | Required | Description |
---|---|---|---|
to | String | Y | RECEIVER_USER_ID |
type | String | Y | In this case, type is text |
text | Object | Y | |
body | String | Y | MESSAGE_CONTENT |
context | Object | Y | |
message_id | String | Y | MESSAGE_ID |
2.1.1.5 Verify Code Message
Example
POST {domain}/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/message
Authorization: ACCESS_TOKEN
Content-Type: application/json
{
"to": "<RECEIVER_USER_ID>",
"type": "vcode",
"vcode": {
"code": "<VERIFY-CODE>",
"remark": "<REMARK-CONTENT>"
}
}
Definition
Name | Type | Required | Description |
---|---|---|---|
to | String | Y | RECEIVER_USER_ID |
type | String | Y | In this case, type is text |
vcode | Object | Y | |
code | String | Y | VERIFY-CODE |
remark | String | Y | REMARK-CONTENT |
2.1.2 Interactive Message
2.1.2.1 Reply Button
Example
POST {domain}/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/message
Authorization: ACCESS_TOKEN
Content-Type: application/json
{
"to": "<RECEIVER_USER_ID>",
"type": "interactive",
"interactive": {
"type": "button",
"header": {
"type": "text" | "image" | "video" | "document",
"text": "your text"
# OR
"document": {
"id": "your-media-id"
}
# OR
"video": {
"id": "your-media-id"
}
# OR
"image": {
"id": "your-media-id"
}
},
"body": {
"text": "<BUTTON_TEXT>"
},
"action": {
"buttons": [
{
"type": "reply",
"reply": {
"id": "UNIQUE_BUTTON_ID_1",
"title": "BUTTON_TITLE_1"
}
},
{
"type": "reply",
"reply": {
"id": "UNIQUE_BUTTON_ID_2",
"title": "BUTTON_TITLE_2"
}
}
]
}
}
}
Definition
Name | Type | Required | Description |
---|---|---|---|
to | String | Y | RECEIVER_USER_ID |
type | String | Y | Required. The type of interactive message you want to send. Supported values: list: Use it for List Messages. button: Use it for Reply Buttons. |
interactive | Object | Y | Refer to the interactive object definition. |
2.1.2.2 List Message
Example
POST {domain}/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/message
Authorization: ACCESS_TOKEN
Content-Type: application/json
{
"to": "<RECEIVER_USER_ID>",
"type": "interactive",
"interactive": {
"type": "list",
"header": {
"type": "text",
"text": "<HEADER_TEXT>"
},
"body": {
"text": "<BODY_TEXT>"
},
"footer": {
"text": "<FOOTER_TEXT>"
},
"action": {
"button": "<BUTTON_TEXT>",
"sections": [
{
"title": "SECTION_1_TITLE",
"rows": [
{
"id": "SECTION_1_ROW_1_ID",
"title": "SECTION_1_ROW_1_TITLE",
"description": "SECTION_1_ROW_1_DESCRIPTION"
},
{
"id": "SECTION_1_ROW_2_ID",
"title": "SECTION_1_ROW_2_TITLE",
"description": "SECTION_1_ROW_2_DESCRIPTION"
}
]
},
{
"title": "SECTION_2_TITLE",
"rows": [
{
"id": "SECTION_2_ROW_1_ID",
"title": "SECTION_2_ROW_1_TITLE",
"description": "SECTION_2_ROW_1_DESCRIPTION"
},
{
"id": "SECTION_2_ROW_2_ID",
"title": "SECTION_2_ROW_2_TITLE",
"description": "SECTION_2_ROW_2_DESCRIPTION"
}
]
}
]
}
}
}
Definition
Name | Type | Required | Description |
---|---|---|---|
to | String | Y | Required. RECEIVER_USER_ID |
type | String | Y | Required. In this case, type is list |
interactive | Object | Y | Required. Refer to the interactive object definition. |
2.1.2.3 Object Definition
2.1.2.3.1 Interactive Object
Name | Type | Required | Description |
---|---|---|---|
type | String | Y | Required. The type of interactive message you want to send. Supported values: - list: Use for List Messages. - button: Use for Reply Buttons. |
header | Object | N | Optional Header content displayed on top of a message. In **list ** interactive messages, you must set the type of the header to text and add a text field that contains the desired content. This object does not exceed 60 characters. In button interactive messages, you can use headers of the following types: text, video, image, or document. For video , image , and document types: Add a media object.For the text type: Add a text field that contains the desired content.Refer to the header object definition for more information. |
body | Object | Y | Required An object with the body of the message. The body object contains the following field:text string – Required if body is present. The content of the message. Maximum length: 1024 characters.Refer to the body object definition for more information. |
footer | Object | N | Optional. An object with the footer of the message. The footer object contains the following field:text string – Required if footer is present. The footer content. Maximum length: 60 characters. Refer to the footer object definition for more information. |
action | Object | Y | Required. Action you want the user to perform after reading the message. Refer to the action object definition for more information. |
2.1.2.3.2 Header Object
Name | Type | Required | Description |
---|---|---|---|
type | String | Y | - text: Used for List Messages, Reply Buttons. - video: Used for Reply Buttons. - image: Used for Reply Buttons. - document: Used for Reply Buttons. |
text | String | N | Required if type is set to text. Maximum length: 60 characters. |
document | Object | N | Required if type is set to document. Contains the media object for this document. eg: "document": { "id": "your-document-id"} |
image | Object | N | Required if type is set to image. Contains the media object for this image. eg: "image": { "id": "your-image-id"} |
video | Object | N | Required if type is set to video. Contains the media object for this video. eg: "video": { "id": "your-video-id"} |
2.1.2.3.3 Body Object
Name | Type | Required | Description |
---|---|---|---|
text | String | Y | Maximum length: 1024 characters. |
2.1.2.3.4 Footer Object
Name | Type | Required | Description |
---|---|---|---|
text | String | Y | Maximum length: 60 characters. |
2.1.2.3.5 Action Object
Name | Type | Required | Description |
---|---|---|---|
button | String | N | Required for List Messages. Button content. It cannot be an empty string and must be unique within the message. Emojis are supported, markdown is not. Maximum length: 20 characters. |
buttons | Objects | N | Required for Reply Buttons. A button object can contain the following parameters: type: only supported type is reply (for Reply Button) title: Button title. It cannot be an empty string and must be unique within the message. Emojis are supported, markdown is not. Maximum length: 20 characters. id: Unique identifier for your button. This ID is returned in the webhook when the button is clicked by the user. Maximum length: 256 characters. You can have up to 3 buttons. You cannot have leading or trailing spaces when setting the ID. Example: “buttons”: [ { “type”: “reply”, “reply”: { “id”: “UNIQUE_BUTTON_ID_1”, “title”: “BUTTON_TITLE_1” } }, { “type”: “reply”, “reply”: { “id”: “UNIQUE_BUTTON_ID_2”, “title”: “BUTTON_TITLE_2” } } ] |
sections | Objects | N | Required for List Messages. Refer to the section object definition. |
2.1.2.3.6 Section Object
Name | Type | Required | Description |
---|---|---|---|
title | String | N | Required if the message has more than one section. Maximum length: 24 characters. |
rows | Objects | N | Required for List Messages. Contains a list of rows. You can have a total of 10 rows across your sections. Each row must have a title (Maximum length: 24 characters) and an ID (Maximum length: 200 characters). You can add a description (Maximum length: 72 characters), but it is optional. Example: “rows”: [ { “id”:“unique-row-identifier-here”, “title”: “row-title-content-here”, “description”: “row-description-content-here”, } ] |
2.1.2.3.7 Row Object
Name | Type | Required | Description |
---|---|---|---|
id | String | Y | unique row identifier (Maximum length: 200 characters) |
title | String | Y | row title content (Maximum length: 24 characters) |
description | String | N | row description content(Maximum length: 72 characters) |
2.1.2.3.8 Media Object
Name | Type | Required | Description |
---|---|---|---|
id | String | Y | Required when type is audio, document, image, or video and you are not using a link. The media object ID. Do not use this field when message type is set to text. |
2.1.3 Template Message
Example
POST {domain}/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/message
Authorization: ACCESS_TOKEN
Content-Type: application/json
{
"to": "<RECEIVER_USER_ID>",
"type": "template",
"template": {
"namespace": "<TEMPLATE_NAMESPACE>"
"name": "<TEMPLATE_NAME>",
"language": {
"code": "<LANGUAGE_AND_LOCALE_CODE>"
},
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": {
"id": "your-image-media-id"
}
}
]
},
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "<TEXT_STRING>"
}
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": "0",
"parameters": [
{
"type": "payload",
"payload": "<PAYLOAD>"
}
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": "1",
"parameters": [
{
"type": "payload",
"payload": "<PAYLOAD>"
}
]
}
]
}
}
Definition
Name | Type | Required | Description |
---|---|---|---|
namespace | String | Y | Template namespace. In this case, use XXX. |
name | String | Y | Ensure unique name within the application. |
language | String | Y | eg: en, ar, zh_CN |
components | Objects | N | The following objects are nested inside the template object: - Button object - Components object - Language object - Parameter object |
2.1.3.1 Object Definition
2.1.3.1.1 Components Object
Name | Type | Required | Description |
---|---|---|---|
type | String | Y | Used to describe the type of component. Values: header, body, and button. |
parameters | Objects | N | Optional. An array containing the contents of the message. |
2.1.3.1.2 Parameter Object
Name | Type | Required | Description |
---|---|---|---|
type | String | Y | Required Used to describe the type of a parameter. Values: text, image, document, and video. |
text | String | N | Required when type=text. The message’s text. Character limit varies based on the component type. |
image | Object | N | Required when type=image. |
document | Object | N | Required when type=document. |
video | Object | N | Required when type=video. |
2.1.3.1.3 Button Parameter Object
Inside the components object, you can set type to button. Here are the button parameters:
Name | Type | Required | Description |
---|---|---|---|
type | String | Y | Required value is button |
sub_type | String | Y | Required the type of button support value: quick_reply, url |
index | String | Y | Required. Button location index. By using an index value of 0-2, you can have up to 3 buttons. |
parameters | String | Y | Required. Button parameters, which are set when created in the business management platform. Add the following parameters to the request: type (required) : Indicates the parameter type of the button. Supported values: payload and text. payload (necessary for the quick_reply button) : In addition to the displayed text on the button, a developer-defined payload is returned when the button is clicked. text (necessary for the url button) : A developer-provided suffix that will be appended to the dynamic URL button created earlier. |
Example of button type with sub_type as quick_reply:
{
"type": "button",
"sub_type": "quick_reply",
"index": 0,
"parameters":
[{
"type": "payload",
"payload": "Yes-Button-Payload"
}]
}
If your template content includes a dynamic url button, this is an example of a button type with sub_type as the url:
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [
{
"type": "text",
"text": "urlValueTest1"
},
{
"type": "text",
"text": "urlValueTest2"
}
]
}
The content of the dynamic url button configuration in the example:
https://xxx.com?var1={{1}}&var2={{2}}
2.1.4 Send Message Response
2.1.4.1 Success Response
Example
{
"code": 0,
"message": "ok",
"message_id": "MESSAGE_ID"
}
Definition
Name | Type | Required | Description |
---|---|---|---|
code | Number | Y | Response code |
message | String | Y | Response message |
message_id | String | N | Required when success |
2.1.4.2 Error Response
Example
{
"type": "AuthException",
"code": 130429,
"message": "Rate limit hit"
}
Definition
Name | Type | Required | Description |
---|---|---|---|
type | String | Y | Refer to Error Type |
code | String | Y | |
message | String | Y |
2.2 Change Message Status
2.2.1 Message Read
Example
POST {domain}/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/message/status
Authorization: ACCESS_TOKEN
Content-Type: application/json
{
"to": "<RECEIVER_USER_ID>",
"context": {
"message_id": "<MESSAGE_ID>"
},
"status": "read"
}
Definition
Name | Type | Required | Description |
---|---|---|---|
status | String | Y | |
context | Object | Y | |
message_id | String | Y | MESSAGE_ID |
3. Media
3.1 Upload Media
Example
curl -X POST 'https://{serverRoot}/official/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/media/upload \
-H 'Authorization: <ACCESS_TOKEN>' \
-F 'file=@"2jC60Vdjn/cross-trainers-summer-sale.jpg"' \
-F 'type="image/jpeg"' \
-F 'messaging_product="<MESSAGING_PRODUCT>"'
-F 'extension="<EXTENSION>"'
Definition
Name | Type | Required | Description |
---|---|---|---|
file | N/A | Y | Maximum file size: 64MB. |
type | N/A | String | image - image/jpeg - image/png document - text/plain - application/pdf - application/vnd.ms-powerpoint - application/msword - application/vnd.ms-excel - application/vnd.openxmlformats-officedocument.wordprocessingml.document - application/vnd.openxmlformats-officedocument.presentationml.presentation - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet audio - audio/aac - audio/mp3 - audio/ogg video - video/mp4 |
messaging_product | String | Y | Required. Messaging service used for the request. In this case, use XXX. |
extension | String | N | For extension only, the original data is returned when retrieved Maximum size: 60K. |
Response
{
"code": 0,
"message": "ok",
"media_id": "<MEIDA_ID>"
}
3.2 Retrieve Media
Example
- Request
GET {serverRoot}/official/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/media/info/{MEIDA-ID}
Authorization: ACCESS_TOKEN
- Response
{
"code": 0,
"message": "ok",
"data": {
"messaging_product": "XXX",
"url": "<URL>",
"mime_type": "<MIME_TYPE>",
"sha256": "<HASH>",
"file_size": "<FILE_SIZE>",
"id": "<MEDIA_ID>",
"extension": "<EXTENSION>"
}
}
Definition
Name | Type | Required | Description |
---|---|---|---|
url | String | Y | |
mime_type | String | Y | |
sha256 | String | Y | |
file_size | Integer | Y | |
id | String | Y | Media ID |
extension | String | N | Media extension info |
3.3 Download Media
Example
GET {serverRoot}/official/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/media/download/{MEIDA-ID}
Authorization: ACCESS_TOKEN
3.4 Delete Media
Example
- Request
DELETE {serverRoot}/official/{API-VERSION}/{OFFICIAL-APPLICATION-ID}/media/{MEIDA-ID}
Authorization: ACCESS_TOKEN
- Response
{
"code": 0,
"message": "ok",
"data": {
"messaging_product": "XXX",
"url": "<URL>",
"mime_type": "<MIME_TYPE>",
"sha256": "<HASH>",
"file_size": "<FILE_SIZE>",
"id": "<MEDIA_ID>",
"extension": "<EXTENSION>"
}
}
4. Webhook
4.1 Webhook Authorization
- Login to the XXX Official Account Developer portal to obtain the webhook token associated with your application.
- The webhook server must to open https and http-keepalive.
Http Example
POST <WEBHOOK_URL>
Authorization: <WEBHOOK-TOKEN>
Content-Type: application/json
{
<WEBHOOK-PAYLOAD_CONTENT>
}
4.2 Webhook Entry Object
Example
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": <VALUE_OBJECT>,
"field": "<FIELD>"
}
]
}
]
}
Definition
Name | Type | Required | Description |
---|---|---|---|
entry | Objects | Y | |
official_application_id | String | Y | |
changes | Objects | Y | |
value | Objects | Y | |
field | String | Y | Notification type, such as: - messages: message webhook - statuses: message status update webhook - message_template_status_update: message template status update webhook |
4.3 Message Webhook
4.3.1 Message Webhook Object
Example
{
"from": "<SENDER_ID>",
"id": "<MESSAGE_ID>",
"timestamp": <TIMESTAMP>,
"type": "<MESSAGE_TYPE>",
"text": {
"body": "<MESSAGE_TEXT>"
},
"attachments": [
{
"type": "<ATTACHMENT_TYPE>",
"payload": {
"url": "<ATTACHMENT_URL>",
}
}
],
"location": {
"latitude": <LATITUDE>,
"longitude": <LONGITUDE>,
"name": "<LOCATION_NAME>",
"address": "<LOCATION_ADDRESS>"
},
"context": {
"message_id": "<MESSAGE_ID>"
},
"reply_to": {
"button_id": "<BUTTON_ID>",
"button_title": "<BUTTON_TITLE>",
"section_id": "<SECTION_ID>",
"row_id": "<ROW_ID>",
"row_title": "<ROW_TITLE>"
}
}
Definition
Name | Required | Type | Description |
---|---|---|---|
from | Y | String | SENDER_ID |
id | Y | String | MESSAGE_ID |
timestamp | Y | Number | TIMESTAMP |
type | Y | String | MESSAGE_TYPE,The type of the reply message can be the same as or different from the original message, depending on the user’s response. For example, it can be a text reply, a reply button, or a reply list. - text - image - audio - video - document - location |
text | N/A | Object | Required for text message |
body | Y | String | |
attachments | N/A | Object | Required for rich media message |
type | Y | String | image - image/jpeg - image/png document - text/plain - application/pdf - application/vnd.ms-powerpoint - application/msword - application/vnd.ms-excel - application/vnd.openxmlformats-officedocument.wordprocessingml.document - application/vnd.openxmlformats-officedocument.presentationml.presentation - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet audio - audio/aac - audio/mp4 - audio/mp3 video - video/mp4 |
payload | Y | Object | |
url | Y | String | |
text | N | String | |
location | N/A | Object | Required for location message |
latitude | Y | Number | |
longitude | Y | Number | |
name | Y | String | |
address | Y | String | |
context | N/A | Object | Required for need to reply to |
message_id | Y | String | |
reply_to | N/A | Object | Required for interactive Message |
button_id | N/A | String | Required for reply button Message |
button_title | N/A | String | Required for reply button Message |
section_id | N/A | String | Required for list Message |
row_id | N/A | String | Required for list Message |
row_title | N/A | String | Required for list Message |
4.3.2 Text Message
Example
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"messages": [
{
"from": "<SENDER_ID>",
"id": "<MESSAGE_ID>",
"timestamp": 1234567890,
"type": "text",
"text": {
"body": "<MESSAGE_TEXT>"
}
}
]
},
"field": "messages"
}
]
}
]
}
4.3.3 Image Message
Example
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"messages": [
{
"from": "<SENDER_ID>",
"id": "<MESSAGE_ID>",
"timestamp": 1234567890,
"type": "image",
"attachments": [
{
"type": "<ATTACHMENT_TYPE>",
"payload": {
"url": "<ATTACHMENT_URL>",
"text":"<TEXT>"
}
}
]
}
]
},
"field": "messages"
}
]
}
]
}
4.3.4 Document Message
Example
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"messages": [
{
"from": "<SENDER_ID>",
"id": "<MESSAGE_ID>",
"timestamp": 1234567890,
"type": "document",
"attachments": [
{
"type": "<ATTACHMENT_TYPE>",
"payload": {
"url": "<ATTACHMENT_URL>",
"text":"<TEXT>"
}
}
]
}
]
},
"field": "messages"
}
]
}
]
}
4.3.5 Audio Message
Example
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"messages": [
{
"from": "<SENDER_ID>",
"id": "<MESSAGE_ID>",
"timestamp": 1234567890,
"type": "audio",
"attachments": [
{
"type": "<ATTACHMENT_TYPE>",
"payload": {
"url": "<ATTACHMENT_URL>",
"text":"<TEXT>"
}
}
]
}
]
},
"field": "messages"
}
]
}
]
}
4.3.6 Video Message
Example
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"messages": [
{
"from": "<SENDER_ID>",
"id": "<MESSAGE_ID>",
"timestamp": 1234567890,
"type": "video",
"attachments": [
{
"type": "<ATTACHMENT_TYPE>",
"payload": {
"url": "<ATTACHMENT_URL>",
"text":"<TEXT>"
}
}
]
}
]
},
"field": "messages"
}
]
}
]
}
4.3.7 Location Message
Example
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"messages": [
{
"from": "<SENDER_ID>",
"id": "<MESSAGE_ID>",
"timestamp": 1234567890,
"type": "location",
"location": {
"latitude": 1234567890,
"longitude": 1234567890,
"name": "<LOCATION_NAME>",
"address": "<LOCATION_ADDRESS>"
}
}
]
},
"field": "messages"
}
]
}
]
}
4.3.8 Reply To Message
Example
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"messages": [
{
"from": "<SENDER_ID>",
"id": "<MESSAGE_ID>",
"timestamp": 1234567890,
"type": "text",
"text": {
"body": "<MESSAGE_TEXT>"
},
"context": {
"message_id": "<MESSAGE_ID>"
}
}
]
},
"field": "messages"
}
]
}
]
}
4.3.9 Reply Button Message
Example
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"messages": [
{
"from": "<SENDER_ID>",
"id": "<MESSAGE_ID>",
"timestamp": 1234567890,
"type": "text",
"context": {
"message_id": "<MESSAGE_ID>"
},
"reply_to": {
"button_id": "<BUTTON_ID>",
"button_title": "<BUTTON_TITLE>"
}
}
]
},
"field": "messages"
}
]
}
]
}
4.3.10 List Message
Example
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"messages": [
{
"from": "<SENDER_ID>",
"id": "<MESSAGE_ID>",
"timestamp": 1234567890,
"type": "text",
"context": {
"message_id": "<MESSAGE_ID>"
},
"reply_to": {
"section_id": "<SECTION_ID>",
"row_id": "<ROW_ID>",
"row_title": "<ROW_TITLE>"
}
}
]
},
"field": "messages"
}
]
}
]
}
4.4 Event Webhook
4.4.1 Message Status Update
4.4.1.1 Message Status Object
Example
{
"id": "<MESSAGE_ID>",
"receiver_id": "<RECEIVER_ID>",
"status": "<status>",
"timestamp": 1234567890
}
Definition
Name | Type | Required | Description |
---|---|---|---|
id | String | Y | MESSAGE_ID |
receiver_id | String | Y | |
status | String | Y | delivered – A webhook is triggered when a message sent by a official has been delivered read – A webhook is triggered when a message sent by a official has been read |
timestamp | Number | Y |
4.4.1.2 Message Delivered
Example
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"statuses": [
{
"id": "<MESSAGE_ID>",
"receiver_id": "<RECEIVER_ID>",
"status": "delivered",
"timestamp": 1234567890
}
]
},
"field": "statuses"
}
]
}
]
}
4.4.1.3 Message Read
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"statuses": [
{
"id": "<MESSAGE_ID>",
"receiver_id": "<RECEIVER_ID>",
"status": "read",
"timestamp": 1234567890
}
]
},
"field": "statuses"
}
]
}
]
}
4.4.2 Template Status Update
4.4.2.1 Template Status Update Object
Definition
Name | Type | Required | Description |
---|---|---|---|
event | String | Y | event type - APPROVED - REJECTED - PAUSED - PENDING_DELETION |
message_template_id | String | Y | |
message_template_name | String | Y | |
message_template_language | String | Y | |
reason | String | Y | |
other_info | Object | N | |
title | String | Y | |
description | String | Y |
4.4.2.2 Template Approved
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"event": "APPROVED",
"message_template_id": "<TEMPLATE_ID>",
"message_template_name": "<TEMPLATE_NAME>",
"message_template_language": "<LANGUAGE_AND_LOCALE_CODE>",
"reason": "NONE"
},
"field": "message_template_status_update"
}
]
}
]
}
4.4.2.3 Template Rejected
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"event": "REJECTED",
"message_template_id": "<TEMPLATE_ID>",
"message_template_name": "<TEMPLATE_NAME>",
"message_template_language": "<LANGUAGE_AND_LOCALE_CODE>",
"reason": "NONE"
},
"field": "message_template_status_update"
}
]
}
]
}
4.4.2.4 Template Paused
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"event": "PAUSED",
"message_template_id": "<TEMPLATE_ID>",
"message_template_name": "<TEMPLATE_NAME>",
"message_template_language": "<LANGUAGE_AND_LOCALE_CODE>",
"reason": "NONE",
"other_info": {
"title": "SECOND_PAUSE",
"description": "Your message template has been paused for 6 hours until Aug 31 at 12:47 AM UTC because it continued to have issues."
}
},
"field": "message_template_status_update"
}
]
}
]
}
4.4.2.5 Template Padding Deletion
{
"entry": [
{
"official_application_id": "<OFFICIAL_APPLICATION_ID>",
"changes": [
{
"value": {
"event": "PENDING_DELETION",
"message_template_id": "<TEMPLATE_ID>",
"message_template_name": "<MY_TEMPLATE_NAME>",
"message_template_language": "en_US",
"reason": "NONE"
},
"field": "message_template_status_update"
}
]
}
]
}
4.5 Retry Policy
If the webhook requests to your server returns an HTTP status code other than 2XX, or if the webhook can not be delivered due to other reasons, XXX Official-Account service will continue to retry sending the request at a lower frequency until the request is successfully delivered. The retry process will continue for a maximum of 24 hours.
5. Error Code
-
Success response
Http Status Code: 2XX -
Failed response
Http Status Code: 400 - 503
Secondary failure codes consist of 6-digit data and can be defined as needed.
5.1 Error Response
Example
{
"type": "AuthException",
"code": 130429,
"message": "Rate limit hit"
}
Definition
Name | Type | Required | Description |
---|---|---|---|
type | String | Y | Refer to Error Type |
code | String | Y | |
message | String | Y |
5.2 Error Type
Error Type | HTTP Status Code | Description |
---|---|---|
BadRequest | 400 | |
AuthException | 401 | |
RequestForbidden | 403 | |
TooManyRequests | 429 | |
InternalServerError | 500 | |
ServiceUnavailable | 503 |