一套模仿WhatsApp消息API的接口设计

背景

前一段模仿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

NameTypeRequiredDescription
toStringYRECEIVER_USER_ID
typeStringYIn this case, type is text
textObjectY
bodyStringYMESSAGE_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

NameTypeRequiredDescription
toStringYRECEIVER_USER_ID
typeStringYIn this case, type is media
mediaObjectY
idStringYMEDIA_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

NameTypeRequiredDescription
toStringYRECEIVER_USER_ID
typeStringYIn this case, type is location
locationObjectY
longitudeNumberYLONG_NUMBER
latitudeNumberYLAT_NUMBER
nameStringYLOCATION_NAME
addressStringYLOCATION_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

NameTypeRequiredDescription
toStringYRECEIVER_USER_ID
typeStringYIn this case, type is text
textObjectY
bodyStringYMESSAGE_CONTENT
contextObjectY
message_idStringYMESSAGE_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

NameTypeRequiredDescription
toStringYRECEIVER_USER_ID
typeStringYIn this case, type is text
vcodeObjectY
codeStringYVERIFY-CODE
remarkStringYREMARK-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

NameTypeRequiredDescription
toStringYRECEIVER_USER_ID
typeStringYRequired.

The type of interactive message you want to send. Supported values:

list: Use it for List Messages.
button: Use it for Reply Buttons.
interactiveObjectYRefer 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

NameTypeRequiredDescription
toStringYRequired.
RECEIVER_USER_ID
typeStringYRequired.
In this case, type is list
interactiveObjectYRequired.
Refer to the interactive object definition.
2.1.2.3 Object Definition
2.1.2.3.1 Interactive Object
NameTypeRequiredDescription
typeStringYRequired.
The type of interactive message you want to send. Supported values:

- list: Use for List Messages.
- button: Use for Reply Buttons.
headerObjectNOptional
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.
bodyObjectYRequired
An object with the body of the message.
The body object contains the following field:
text stringRequired if body is present. The content of the message. Maximum length: 1024 characters.
Refer to the body object definition for more information.
footerObjectNOptional. An object with the footer of the message.
The footer object contains the following field:
textstringRequired if footer is present. The footer content. Maximum length: 60 characters.
Refer to the footer object definition for more information.
actionObjectYRequired.
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
NameTypeRequiredDescription
typeStringY- text: Used for List Messages, Reply Buttons.
- video: Used for Reply Buttons.
- image: Used for Reply Buttons.
- document: Used for Reply Buttons.
textStringNRequired if type is set to text.
Maximum length: 60 characters.
documentObjectNRequired if type is set to document.
Contains the media object for this document.
eg:
"document": { "id": "your-document-id"}
imageObjectNRequired if type is set to image.
Contains the media object for this image.
eg:
"image": { "id": "your-image-id"}
videoObjectNRequired 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
NameTypeRequiredDescription
textStringYMaximum length: 1024 characters.
2.1.2.3.4 Footer Object
NameTypeRequiredDescription
textStringYMaximum length: 60 characters.
2.1.2.3.5 Action Object
NameTypeRequiredDescription
buttonStringNRequired 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.
buttonsObjectsNRequired 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”
}
}
]
sectionsObjectsNRequired for List Messages.
Refer to the section object definition.
2.1.2.3.6 Section Object
NameTypeRequiredDescription
titleStringNRequired if the message has more than one section.
Maximum length: 24 characters.
rowsObjectsNRequired 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
NameTypeRequiredDescription
idStringYunique row identifier (Maximum length: 200 characters)
titleStringYrow title content (Maximum length: 24 characters)
descriptionStringNrow description content(Maximum length: 72 characters)
2.1.2.3.8 Media Object
NameTypeRequiredDescription
idStringYRequired 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

NameTypeRequiredDescription
namespaceStringYTemplate namespace. In this case, use XXX.
nameStringYEnsure unique name within the application.
languageStringYeg: en, ar, zh_CN
componentsObjectsNThe 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
NameTypeRequiredDescription
typeStringYUsed to describe the type of component.
Values: header, body, and button.
parametersObjectsNOptional.

An array containing the contents of the message.
2.1.3.1.2 Parameter Object
NameTypeRequiredDescription
typeStringYRequired
Used to describe the type of a parameter.
Values: text, image, document, and video.
textStringNRequired when type=text.
The message’s text. Character limit varies based on the component type.
imageObjectNRequired when type=image.
documentObjectNRequired when type=document.
videoObjectNRequired 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:

NameTypeRequiredDescription
typeStringYRequired
value is button
sub_typeStringYRequired
the type of button
support value: quick_reply, url
indexStringYRequired.

Button location index.
By using an index value of 0-2, you can have up to 3 buttons.
parametersStringYRequired.

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

NameTypeRequiredDescription
codeNumberYResponse code
messageStringYResponse message
message_idStringNRequired when success
2.1.4.2 Error Response

Example

{
    "type": "AuthException",
    "code": 130429,
    "message": "Rate limit hit"
}

Definition

NameTypeRequiredDescription
typeStringYRefer to Error Type
codeStringY
messageStringY

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

NameTypeRequiredDescription
statusStringY
contextObjectY
message_idStringYMESSAGE_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

NameTypeRequiredDescription
fileN/AYMaximum file size: 64MB.
typeN/AStringimage
- 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_productStringYRequired.
Messaging service used for the request. In this case, use XXX.
extensionStringNFor 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

NameTypeRequiredDescription
urlStringY
mime_typeStringY
sha256StringY
file_sizeIntegerY
idStringYMedia ID
extensionStringNMedia 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

  1. Login to the XXX Official Account Developer portal to obtain the webhook token associated with your application.
  2. 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

NameTypeRequiredDescription
entryObjectsY
official_application_idStringY
changesObjectsY
valueObjectsY
fieldStringYNotification 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

NameRequiredTypeDescription
fromYStringSENDER_ID
idYStringMESSAGE_ID
timestampYNumberTIMESTAMP
typeYStringMESSAGE_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
textN/AObjectRequired for text message
bodyYString
attachmentsN/AObjectRequired for rich media message
typeYStringimage
- 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
payloadYObject
urlYString
textNString
locationN/AObjectRequired for location message
latitudeYNumber
longitudeYNumber
nameYString
addressYString
contextN/AObjectRequired for need to reply to
message_idYString
reply_toN/AObjectRequired for interactive Message
button_idN/AStringRequired for reply button Message
button_titleN/AStringRequired for reply button Message
section_idN/AStringRequired for list Message
row_idN/AStringRequired for list Message
row_titleN/AStringRequired 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

NameTypeRequiredDescription
idStringYMESSAGE_ID
receiver_idStringY
statusStringYdelivered – 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
timestampNumberY
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

NameTypeRequiredDescription
eventStringYevent type
- APPROVED
- REJECTED
- PAUSED
- PENDING_DELETION
message_template_idStringY
message_template_nameStringY
message_template_languageStringY
reasonStringY
other_infoObjectN
titleStringY
descriptionStringY
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

NameTypeRequiredDescription
typeStringYRefer to Error Type
codeStringY
messageStringY

5.2 Error Type

Error TypeHTTP Status CodeDescription
BadRequest400
AuthException401
RequestForbidden403
TooManyRequests429
InternalServerError500
ServiceUnavailable503
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BossFriday

原创不易,请给作者打赏或点赞!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值