# Associate external options

When configuring the approval form definition, single-select and multi-select controls support associating external options by inputting data from external systems into the form control, using them as control option values. This method allows the data from multiple systems within an enterprise to be linked, enabling employees to automatically obtain the latest and most comprehensive data during the approval process.

## Feature introduction

If single-select and multi-select controls are used in the approval form design, you can manually add options based on actual situations or use external options.

- **Manually Add Options**: Add multiple options manually to the form design, with fixed option values. Adjustments must be made by updating the approval form design.
- **Use External Options**: Transmit data from external systems into the form control to automatically generate corresponding options. Single-select and multi-select controls designed in this way can dynamically update options based on changes in external system data, thereby reducing approval definition maintenance costs. This is suitable for enterprises that maintain multiple systems simultaneously (Feishu Approval, HR systems, Sales Management systems, etc.), where system linkage ensures data synchronization to the approval form as options.

For example, if Feishu Approval initiates an approval involving sales, the salesperson needs to fill out an external customer list when submitting approval. The list is maintained in a sales management system and often changes. By configuring external data as single-select or multi-select options, the salesperson can select the customers they are tracking when submitting approval. Changes in the sales management system data will also synchronize updates to the approval system, eliminating the need for repeated maintenance.

When designing the **Single select** and **Multiple select** controls in the Feishu Approval Center, the **Use external options** configuration is shown as follows:

![image.png](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/3a88759d747021c0a4e2badf9ec0ea16_culUYmiNKN.png?height=1506&lazyload=true&maxWidth=600&width=2160)

Configuration Process:

1. Enterprise developers write the relevant data interface according to the external option interface specification provided in this document and provide a request URL accessible by the Feishu Approval Center.
2. Enterprise approval administrators go to the Feishu Approval Management Backend and input the external option interface parameter configurations into the single-select and multi-select controls of the specified approval definition based on the interfaces developed by the developers. Then, verify if the interface configuration is successful.warning
    - The external option data interface's request URL and custom Token must be provided to verify if the request source is legitimate.
    - A Key can be optionally provided. If a Key is provided, data needs to be encrypted and decrypted during transmission. If not, data will be transmitted in plaintext without encryption.

![image.png](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/18d628d72388068ef5184ff93e8bd021_c1gFatOljH.png?height=1506&lazyload=true&maxWidth=600&width=2138)

## Feature advantages

Compared to manually adding options, using external options offers the following advantages:

- The same data does not need to be repeatedly updated or modified in multiple systems, reducing maintenance costs.
- Regardless of the number of options, after interface development, employees only need to select options relevant to themselves via control parameter configuration.

## External option interface

You need to develop an HTTP or HTTPS interface for associating external options based on the interface specifications provided in this section.

- The implementation of this interface is not restricted by the development language.
- You need to properly set the Token and Key parameters (parameter formats are not restricted, as long as they match the Token and Key provided in the Feishu Approval Center form design).

- Token is used to verify the request source.
    - Key is used for encryption and decryption. Key is an optional parameter; if not provided, encryption will not be performed.warning
- Feishu Approval does not guarantee the correctness of the document and does not rectify data issues caused by unsatisfactory data source interface returns, unstable data source interfaces, or interface unavailability, etc.
- Linkage parameters (corresponding to linkage_params) and V2 version usage (corresponding to page_token, query parameters) are currently not fully supported by the open platform. 

### Interface call method

If the form is in editing status and the data source is an external system widget, the approval system will initiate an **HTTP or HTTPS** request to the external data source API address when a user clicks to validate data or initiates a request. It is necessary to configure the interface address accessible to the public network, but not the intranet address, and the interface needs to be faster, otherwise the network jitter will easily lead to timeout.

- **Request address**: The request address configured by the user

- **Request mode**: POST

- **Request timeout duration**: 3 sec

- **Request header**:

key|value
  --|--
  Content-Type|application/json

### Request parameters

Currently, approval supports requesting different data for single-select and multi-select controls via the `user_id`, `employee_id`, and `extra` fields (linkage parameter fields) associated within the form. When both `user_id` and `employee_id` are empty, all options are returned. `user_id` and `employee_id` will be set to the initiator's ID when the approval process is initiated. The request parameter format for single-select and multi-select controls is as follows:

```json 
{
        "user_id": "123",
        "employee_id": "abc",
        "token":"1e8e999f580e7a202dbe1e5103c5e4c58ecc757e",
        "linkage_params":{
          "key1":"value1", // key1 is the field code of the linked field, and value1 is the linked widget value.
          "key2":"value2", // key2 is the field code of the linked field, and value2 is the linked widget value.
          "page_token":"xxxxx", // If not provided or left empty, the first page of data will be returned.
          "query":"beijing", // Search Keywords.
          "locale":"zh_cn" // User's Current Language Environment.
        }
} 
```
**Request parameter description**:  

Parameter | Type | Required | Description
---|---|---|---
user_id | String | No | This parameter corresponds to the internal ID, so it is recommended to use the employee_id parameter to pass the user ID.<br>**Note**: If neither user_id nor employee_id are provided, it means that all data is expected to be returned.
employee_id | String | No | employee_id corresponds to the user's user_id. For how to obtain it, please refer to [How to Obtain User ID](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-user-id).<br>- When initiating approval, pass the initiator's employee_id to determine the data range to be returned.<br>- If neither user_id nor employee_id are provided, it means that all data is expected to be returned.
token | String | Yes | Custom value used to validate whether the request is from a legitimate source.
linkage_params | Map | No | Linkage option parameters (if linkage_params is not provided, return all options). When linkage options are set, the linkage parameters are placed in a map and sent with the request upon option selection. You should decide the data to return based on this parameter.<br>![image.png](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/0353eb4eddc1d85e6277e4f287f3637d_hY4BQ8oaq4.png?height=1504&lazyload=true&width=2128)
page_token | String | No | Pagination token, leave blank for the first request to start from the beginning.<br>- When there are more items in the pagination query result, the interface will return a new page_token, which can be used for the next iteration to obtain the query result.<br>- The amount of data returned with each request (page size) is no less than 10.<br>- This is only valid for data sources that support **fuzzy search and paged search**.<br>![image.png](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/3ef028acd59f3ba195bb403ecc3c1b99_X0vKi8wGLR.png?height=1532&lazyload=true&width=2114)
query | String | No | Search keywords, only valid for data sources that support **fuzzy search and paged search**.
locale | String | No | Language environment, only valid for data sources that support **fuzzy search and paged search**. Values:<br>- zh_cn: Chinese<br>- en_us: English<br>- ja_jp: Japanese

### Returned parameter format

Format of output parameters before encryption:

```js 
{
    "code":0,
    "msg":"success!",
    "data":{
        "result":{
            "options":[
                {
                    "id":"options_1_id_1",
                    "value":"@i18n@options_1_name_1",
                    "isDefault":true
                },
                {
                    "id":"options_1_id_2",
                    "value":"@i18n@options_1_name_2"
                },
                {
                    "id":"options_1_id_3",
                    "value":"@i18n@options_1_name_3"
                }
            ],
            "i18nResources":[
                {
                    "locale":"zh_cn",
                    "isDefault":true,
                    "texts":{
                        "@i18n@options_1_name_1":"Value 1",
                        "@i18n@options_1_name_2":"Value 2",
                        "@i18n@options_1_name_3":"Value 3"
                    }
                },
                {
                    "locale":"en_us",
                    "isDefault":false,
                    "texts":{
                        "@i18n@options_1_name_1":"value1",
                        "@i18n@options_1_name_2":"value2",
                        "@i18n@options_1_name_3":"value3"
                    }
                }
            ],
            "hasMore":true,
            "nextPageToken": "xxxx"
        }
    }
} 
```

**Return parameter description**: (i18nResources must be returned, returning empty will cause the display to be empty)
|Parameter|Type|Description|
|-|-|-|
|code|int|Error code. A value other than 0 indicates failure.|
|msg|string|Return code description|
|data|object|Returned business information|
|&emsp;∟result|object|Request result content|
|&emsp;&emsp;∟options|list<externalData>|Option list|
|&emsp;&emsp;∟i18nResources|list<i18nResource>|Internationalized text|
|&emsp;&emsp;∟hasMore|bool|Whether the response body has more parameters. Only valid for V2 - Supports fuzzy search and paged search|
|&emsp;&emsp;∟nextPageToken|string|Page identifier, when has_more is true, a new nextPageToken will also be returned. Only valid for V2 - Supports fuzzy search and paged search|
**externalData structure**: 
|Parameter|Type|Description|
|-|-|-|
|id|string|Option unique identity (Globally unique and fixed)|
|value|string|The key displayed by the option (to be guaranteed to be globally unique and fixed) matches the displayed copy in the text of i18nResources through this Key and the locale of the current client.|
  |isDefault|bool|Default Option or Not|

**i18nResource structure**: 
|Parameter|Type|Description|
|-|-|-|
|locale|string|Language (zh_cn: Chinese, en_us: English, ja_jp: Japanese)
|isDefault|bool|Whether it is a default option
|texts|map[string]string|Internationalized text map. key is the unique value of an internationalized option. This value is the same in different language environments. value is the text for a specific language environment.

<br>

Format of output parameters after encryption (Encrypt the result content and convert it to base64 output, and return it in plaintext without configuring the key):

```js 
{
    "code":0,
    "msg":"success!",
    "data":{
        "result":"tKqgkBNFEzakJAeS/ySKS7j7YoX2rKVuzLJbG44xHsz0eHaqLx6ZLsAQ/ljfK9mDi0F/32UVXM3gUQaczHbR2upD/EStb+O26FApdvNKm0yvKG0WrhFIe7UCMkrxPnegBqqgqcMHLCZQZ2uh/2k5dDlhReT6fxm/bAR4ZwgyvvshqudakKigshSK0Aq25IQ0H65PS/5iRHgk2b06sahZuvH6b9yrfBXJqHdhztvPkPW2FkipbvLMrzQdXz+deBm2DTJ5W53f2QKOxk7szaXKOr1+u1MyCIkjldPcAHqPYRiOzx6iXQPJ6hMj7MHex08amm44d5T3Z2jzCoinkGSrhpusTcmhHmQnjDjl51a2LqBlty1L9yHuMaED+al2lTUhlzGHqhITCQBJLZraOkXYcR6oOXAV3gP4towZw5G/zeeEtXYZvWUvTZ9F3UAXM4jP"
    }
} 
```

**Return parameter description**: 
|Parameter|Type|Description|
|-|-|-|
|code|int|Error code. A value other than 0 indicates failure.|
|msg|string|Return code description|
|data|string|Returned business data|
|&emsp;∟result|string|Request result content encrypted in base64|

### Encryption/Decryption method

Golang encryption code is shown below:

```js 
//AES CBC encryption
func CBCEncrypter(buf []byte, keyStr string) ([]byte, error) {
	key := sha256.Sum256([]byte(keyStr))
	plaintext := standardizeDataEn(buf)

if len(plaintext)%aes.BlockSize != 0 {
		return nil, errors.New("plaintext is not a multiple of the block size")
	}

block, err := aes.NewCipher(key[:sha256.Size])
	if err != nil {
		return nil, err
	}

ciphertext := make([]byte, aes.BlockSize+len(plaintext))
	iv := ciphertext[:aes.BlockSize]
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return nil, err
	}

mode := cipher.NewCBCEncrypter(block, iv)
	mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)

return ciphertext, nil
}

func standardizeDataEn(data []byte) []byte {
	appendingLen := aes.BlockSize - (len(data) % aes.BlockSize)
	sd := make([]byte, len(data)+appendingLen)
	copy(sd, data)
	for i := 0; i < appendingLen; i++ {
		sd[i+len(data)] = byte(appendingLen)
	}
	return sd
}
```

<br>
 Java encryption code example:

```js 
 public String CBCEncrypter(String key, String source){
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            messageDigest.reset();
            messageDigest.update(key.getBytes());

SecretKeySpec skeySpec = new SecretKeySpec(messageDigest.digest(), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");//"Algorithm/Mode/Complement method"
            byte[] sSrcBytes = source.getBytes();
            byte[] newSrc =  new byte[sSrcBytes.length + 16];
            byte[] cSrc = new byte[16];
            System.arraycopy(cSrc, 0, newSrc, 0, cSrc.length);
            System.arraycopy(sSrcBytes, 0, newSrc, 16, sSrcBytes.length);
            IvParameterSpec iv = new IvParameterSpec(cSrc);//Using CBC mode, a vector iv is required, which can increase the strength of the encryption algorithm
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
            byte[] encrypted = cipher.doFinal(newSrc);
            return Base64.getEncoder().encodeToString(encrypted);//Here, Base64 is used for transcoding and can be used for secondary encryption
        } catch (Exception e) {
            //handle Exception
        }
        return null;
    } 
```

<br>
The decryption code is shown below:

```js 
//AES CBC decryption
func CBCDecrypter(buf []byte, keyStr string) ([]byte, error) {
	key := sha256.Sum256([]byte(keyStr))
	if len(buf)%aes.BlockSize != 0 {
		return nil, errors.New("plaintext is not a multiple of the block size")
	}
	block, err := aes.NewCipher(key[:sha256.Size])
	if err != nil {
		return nil, err
	}
	ciphertext := make([]byte, aes.BlockSize+len(buf))
	iv := ciphertext[:aes.BlockSize]
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return nil, err
	}
	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(ciphertext[aes.BlockSize:], buf)
	ciphertext = ciphertext[32:]

plain := standardizeDataDe(ciphertext)
	return plain, nil
}

func standardizeDataDe(origData []byte) []byte {
	length := len(origData)
	unpadding := int(origData[length-1])
	if unpadding > length {
		return nil
	}
	return origData[:(length - unpadding)]
}

func RandKey256() (string, error) {
	key := make([]byte, 32)

if _, err := rand.Read(key); err != nil {
		return "", err
	} else {
		return  string(key), nil
	}
} 
```

```js
//Java decryption
/**
 * Use the first 16 bytes of randomly generated IV to decrypt, which is more universal.
 * @param key 
 * @param source cipher text
 * @return clear text
 */
public static String CBCDecrypter(String key, String source){
	try {
		byte[] ciphertext = Base64.getDecoder().decode(source); // BASE64 decryption
		MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
		messageDigest.reset();
		messageDigest.update(key.getBytes());
		SecretKeySpec skeySpec = new SecretKeySpec(messageDigest.digest(), "AES");
		Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // "Algorithm/Mode/Complement Method"
		// Extract the IV from the first 16 bytes of the ciphertext
		byte[] ivBytes = new byte[16];
		System.arraycopy(ciphertext, 0, ivBytes, 0, ivBytes.length);
		IvParameterSpec iv = new IvParameterSpec(ivBytes); // Vector iv
		// Extract the content after 16 bytes of the ciphertext, that is, the real ciphertext after removing IV
		byte[] actualCiphertext = new byte[ciphertext.length - ivBytes.length];
		System.arraycopy(ciphertext, ivBytes.length, actualCiphertext, 0, actualCiphertext.length);
		cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
		byte[] decrypted = cipher.doFinal(actualCiphertext);
		return new String(decrypted);
	} catch (Exception e) {
	}
	return null;
}
```

