Macro Facial Analysis

Description

Provide an image file or image URL to perform face detection and facial attribute analysis.

Version

2.0

Image Requirements

Image formats: JPG (JPEG), PNG Image dimensions: minimum 48 × 48 pixels; maximum 4096 × 4096 pixels Maximum image file size: 10 MB Minimum face dimensions: The system detects a face using a square bounding box. The minimum side length of this square is 1/48 of the shorter image dimension and must be at least 48 pixels. For example, for a 4096 × 3200-pixel image, the minimum face dimensions are 66 × 66 pixels. Maximum number of faces: 10

API URL

    Production environment: https://openapi.meitu.com
    Task submission endpoint: https://openapi.meitu.com/api/v1/sdk/sync/push
    Task name (task): /v1/Macro_facial_analysis/487163
    Task type (task_type): formula

Method

POST

Content-Type: application/json

Authentication

Open Platform API Signature

Credits cannot be used to pay for API calls. Please visit the MV console to activate this API and purchase or manage resource packages.

Request Parameters

RequiredParameterTypeDescription
YesparamsstringAlgorithm parameters (JSON string)
Yesinit_imagesobject[]Media file list
YestaskstringFixed value: /v1/Macro_facial_analysis/487163
Yestask_typestringFixed value: formula
Nosync_timeoutintSynchronous timeout; default: 30

The init_images media file parameters are structured as follows:

RequiredParameterTypeDescription
YesurlstringImage URL or Base64-encoded data
YesprofileobjectAttribute information

profile parameter description

RequiredParameterTypeDescription
Yesmedia_profilesobjectMedia file attribute information
YesversionstringFixed value: v1

Parameters in media_profiles

RequiredParameterTypeDescription
Yesmedia_data_typestringurl indicates a URL; formats such as jpg indicate Base64-encoded data

params inference parameter description

params is a JSON string. After parsing, it has the following structure:

{
  "rsp_media_type": "url",
  "parameter": {
    "return_attributes": "gender,age,race,beauty,glasses,mustache,emotion,eyelid,poseestimation"
  }
}
RequiredFieldTypeDescription
Norsp_media_typestringDefault: url; jpg indicates Base64-encoded data
YesparameterobjectCore algorithm parameter object

parameter description

RequiredParameterTypeDescription
Noreturn_attributesStringSpecifies whether to detect and return attributes inferred from facial features, such as age, gender, smile, and race. List the attributes to detect in a comma-separated string. Attribute order is unrestricted, and spaces must not be added between attributes. If this field is absent or empty, no attributes are detected.
Noreturn_landmarkIntSpecifies whether to detect and return keypoints for facial features and contours. Default: 0

Allowed values for return_attributes

ValueMeaning
genderGender
ageAge
raceRace
beautyAttractiveness score
glassesEyewear
mustacheFacial hair
emotionEmotion
eyelidEyelid type
poseestimationFace pose information

Allowed values for return_landmark

ValueMeaning
0Do not return facial keypoints
1Return 106 normalized keypoints
2Return 106 absolute-coordinate keypoints
3Return 118 normalized keypoints
4Return 118 absolute-coordinate keypoints
5Return 171 normalized keypoints
6Return 171 absolute-coordinate keypoints

Request Example

{
  "task": "/v1/Macro_facial_analysis/487163",
  "task_type": "formula",
  "init_images": [
    {
      "url": "https://obs.mtlab.meitu.com/public/EtRGxLI8ulQ43pGhuZfym9aYUD2PUbMP/MTcwOTE4MjgwMA==/58fdb25d-8e6f-4026-4a5e-45d9894190a4.png",
      "profile": {
        "media_profiles": {
          "media_data_type": "url"
        },
        "version": "v1"
      }
    }
  ],
  "params": "{\"rsp_media_type\":\"url\",\"parameter\":{\"return_attributes\":\"gender,age,race,beauty,glasses,mustache,emotion,eyelid,poseestimation\",\"return_landmark\":0}}",
  "sync_timeout": 30
}

Response Fields

Generated results are periodically deleted. Download and save them promptly.
FieldTypeDescription
request_idStringRequest ID
trace_idStringTrace ID
codeNumberHTTP-layer status code; 0 indicates success
error_codeNumberBusiness error code; 0 indicates success. See API Error Codes
messageStringResponse message; success on success
tipsanyAdditional information; may be null
dataObjectBusiness data returned by the API

data field description

FieldTypeDescription
statusNumberStatus code: -1 task not found; 0 created successfully; 1 processing; 2 failed; 9 timed out—use the Query API; 10 succeeded
resultObjectTask processing result
progressNumberTask progress, ranging from 0 to 1; 1 indicates completion
predict_elapsedNumberEstimated processing time, in milliseconds
create_timeNumberTask creation timestamp, in milliseconds
task_idStringTask ID
custom_task_idStringCustom task ID
trace_idStringTrace ID
client_infoStringClient information
init_imagesArrayOriginal input media information

result field description

FieldTypeDescription
idStringUnique task ID
urlsArrayOutput media URL list
parametersObjectRequest parameters. Example: {"landmark_type":0,"version":"2.0.0"}
dataObjectTask result data
imagesArrayOutput image URL list
mtlab_resObjectRaw response result (same content as data)
media_info_listArrayMedia information list

media_info_list field description

FieldTypeDescription
media_dataStringMedia data (Base64-encoded data or an empty string)
media_extraObjectAdditional media information
media_profilesObjectMedia profile information; usually empty

faces field description

FieldTypeDescription
face_attributesObjectFacial attributes. See the following table for details.
face_rectangleObjectFace bounding-box position. All values are proportions:
left is the x-coordinate of the box's top-left corner as a proportion of the image width; top is the y-coordinate as a proportion of the image height; width is the box width as a proportion of the image width; height is the box height as a proportion of the image height.
face_landmarkListFacial keypoint coordinate array

face_attributes field description

FieldTypeDescription
genderObject"gender": {"value": 1} Gender analysis result. A value of 1 indicates male, and 0 indicates female.
ageObject "age": {"value": 21} Age analysis result. value is a non-negative integer representing the estimated age.
raceObject"race":{"value": 0} Race analysis result. A value of 0 indicates White, 1 Asian, 2 Black, 3 North Indian, 4 South Indian, and 5 Southeast Asian.
beauty_scoreObject"beauty_score":{"value": 90} Attractiveness score, ranging from 0 to 100.
emotionObject"emotion":{"value": 0} Emotion analysis result. A value of 0 indicates sadness, 1 neutral, 2 smiling, 3 laughing, 4 surprise, 5 fear, 6 anger, and 7 disgust.
right_eyelidObject"right_eyelid":{"value": 0} Right-eyelid analysis result. A value of 0 indicates a single eyelid, 1 a double eyelid, and 2 an inner double eyelid.
left_eyelidObject"left_eyelid":{"value": 0} Left-eyelid analysis result. A value of 0 indicates a single eyelid, 1 a double eyelid, and 2 an inner double eyelid.
glasses_typeObject"glasses_type":{"value": 0} Eyewear type analysis result. A value of 0 indicates no glasses, 1 regular glasses, and 2 sunglasses.
glasses_thickObject"glasses_thick":{"value": 0} Frame thickness analysis result. A value of 0 indicates no glasses, 1 a thin frame, and 2 a thick frame.
glasses_sizeObject"glasses_size":{"value": 0} Frame size analysis result. A value of 0 indicates no glasses, 1 a large frame, and 2 a small frame.
glasses_frameObject"glasses_frame":{"value": 0} Full-rim/semi-rimless/rimless analysis result. A value of 0 indicates no glasses, 1 rimless, 2 full-rim, and 3 semi-rimless.
glasses_shapeObject"glasses_shape":{"value": 0} Frame shape analysis result. A value of 0 indicates no glasses, 1 another shape, 2 a square frame, and 3 a round frame.
mustache_typeObject"mustache_type":{"value": 0} Facial hair type analysis result. A value of 0 indicates no facial hair, 1 stubble, and 2 a mustache or beard.
mustache_lengthObject"mustache_length":{"value": 0} Facial hair length analysis result. A value of 0 indicates no facial hair, 1 short, 2 medium, and 3 long.
mustache_shapeObject"mustache_shape":{"value": 0} Facial hair shape analysis result. A value of 0 indicates no facial hair, 1 a partial goatee, 2 a full goatee, 3 a handlebar mustache, 4 a full beard, and 5 sideburns.
mustache_thickObject"mustache_thick":{"value": 0} Facial hair density analysis result. A value of 0 indicates no facial hair, 1 sparse, and 2 dense.
roll_angleObject"roll_angle": {"value": 1.1601325273513794} Face roll angle. value is the roll angle in degrees.
yaw_angleObject"yaw_angle": {"value": 6.59846305847168} Face yaw angle. value is the yaw angle in degrees.
pitch_angleObject"pitch_angle": {"value": 13.970479011535645} Face pitch angle. value is the pitch angle in degrees.
translate_xObject"translate_x": {"value": -10.500459671020508} Face translation along the X axis. value is the X-axis translation.
translate_yObject"translate_y": {"value": 1.797804832458496} Face translation along the Y axis. value is the Y-axis translation.
translate_zObject"translate_z": {"value": 417.212646484375} Face translation along the Z axis. value is the Z-axis translation.

Note: Euler angles use the center as the origin, while the 3D coordinate system uses the top-left corner as the origin.

face_rectangle field description

FieldTypeDescription
leftFloatX-coordinate of the face box's top-left corner (as a proportion of image width)
topFloatY-coordinate of the face box's top-left corner (as a proportion of image height)
widthFloatFace box width (as a proportion of image width)
heightFloatFace box height (as a proportion of image height)

Allowed values for return_landmark

ValueMeaning
0Do not return facial keypoints
1Return 106 normalized keypoints
2Return 106 absolute-coordinate keypoints
3Return 118 normalized keypoints
4Return 118 absolute-coordinate keypoints
5Return 171 normalized keypoints
6Return 171 absolute-coordinate keypoints

Failure Response Fields

FieldTypeDescription
dataStringResponse content
messageStringError message
error_codeNumberError code

API-Specific Error Codes and Messages

ErrorCodeError MessageDescription
20001PROCESS_ERRORProcessing error

Response Examples

Successful Response Example

{
  "request_id": "",
  "trace_id": "",
  "code": 0,
  "error_code": 0,
  "message": "success",
  "tips": null,
  "data": {
    "status": 10,
    "result": {
      "id": "t_mt1a3i5n7b2e53ffab-xxxx-4802-a5ff-dc812744ffa7",
      "media_info_list": [
        {
          "media_data": "",
          "media_extra": {
            "faces": [
              {
                "face_attributes": {
                  "age": {"value": 17},
                  "gender": {"value": 0}
                },
                "face_rectangle": {
                  "height": 0.052405804,
                  "left": 0.37931174,
                  "top": 0.26455134,
                  "width": 0.069874406
                }
              }
            ]
          },
          "media_profiles": null
        }
      ]
    },
    "progress": 1,
    "predict_elapsed": 10000,
    "create_time": 1770290367382,
    "task_id": "t_mt1a3i5n7b2e53ffab-xxxx-4802-a5ff-dc812744ffa7",
    "custom_task_id": "",
    "trace_id": "",
    "client_info": "",
    "init_images": null
  }
}

Pending Response Example

Use the Query API to retrieve the result.

Response Status: 200

Content-Type: application/json; charset=utf-8

{
  "request_id": "",
  "trace_id": "",
  "code": 0,
  "error_code": 0,
  "message": "success",
  "tips": null,
  "data": {
    "status": 9,
    "result": {
      "id": "t_mt1a3i5n7b2e53ffab-xxxx-4802-a5ff-dc812744ffa7"
    },
    "progress": 0,
    "predict_elapsed": 10000,
    "create_time": 1770290367382,
    "task_id": "t_mt1a3i5n7b2e53ffab-xxxx-4802-a5ff-dc812744ffa7",
    "custom_task_id": "",
    "trace_id": "e3642be0-xxxx-41ba-af05-92098703fd58",
    "client_info": "",
    "init_images": null
  }
}

Complete Successful Algorithm Response Example

Response Status: 200; Content-Type: application/json; charset=utf-8

{
  "request_id": "",
  "trace_id": "",
  "code": 0,
  "error_code": 0,
  "message": "success",
  "tips": null,
  "data": {
    "status": 10,
    "result": {
      "id": "t_mt1a3i5n7b2e53ffab-xxxx-4802-a5ff-dc812744ffa7",
      "urls": [
        ""
      ],
      "parameters": {
        "landmark_type": 0,
        "version": "2.0.0"
      },
      "data": {
        "ErrorCode": 0,
        "ErrorMsg": "",
        "error_code": 0,
        "error_msg": "",
        "media_info_list": [
          {
            "media_data": "",
            "media_extra": {
              "faces": [
                {
                  "face_attributes": {
                    "age": {
                      "value": 17
                    },
                    "beauty_score": {
                      "value": 87
                    },
                    "emotion": {
                      "value": 1
                    },
                    "gender": {
                      "value": 0
                    },
                    "glasses_frame": {
                      "value": 0
                    },
                    "glasses_shape": {
                      "value": 0
                    },
                    "glasses_size": {
                      "value": 0
                    },
                    "glasses_thick": {
                      "value": 0
                    },
                    "glasses_type": {
                      "value": 0
                    },
                    "left_eyelid": {
                      "value": 1
                    },
                    "mustache_length": {
                      "value": 0
                    },
                    "mustache_shape": {
                      "value": 0
                    },
                    "mustache_thick": {
                      "value": 0
                    },
                    "mustache_type": {
                      "value": 0
                    },
                    "pitch_angle": {
                      "value": -2.675837516784668
                    },
                    "race": {
                      "value": 1
                    },
                    "right_eyelid": {
                      "value": 2
                    },
                    "roll_angle": {
                      "value": -7.028405666351318
                    },
                    "translate_x": {
                      "value": -118.0142822265625
                    },
                    "translate_y": {
                      "value": -418.3360595703125
                    },
                    "translate_z": {
                      "value": 2067.922119140625
                    },
                    "yaw_angle": {
                      "value": -27.13144302368164
                    }
                  },
                  "face_rectangle": {
                    "height": 0.052405804,
                    "left": 0.37931174,
                    "top": 0.26455134,
                    "width": 0.069874406
                  }
                }
              ]
            },
            "media_profiles": null
          }
        ],
        "msg_id": "",
        "parameter": {
          "landmark_type": 0,
          "version": "2.0.0"
        }
      },
      "images": [
        ""
      ],
      "mtlab_res": {
        "ErrorCode": 0,
        "ErrorMsg": "",
        "error_code": 0,
        "error_msg": "",
        "media_info_list": [
          {
            "media_data": "",
            "media_extra": {
              "faces": [
                {
                  "face_attributes": {
                    "age": {
                      "value": 17
                    },
                    "beauty_score": {
                      "value": 87
                    },
                    "emotion": {
                      "value": 1
                    },
                    "gender": {
                      "value": 0
                    },
                    "glasses_frame": {
                      "value": 0
                    },
                    "glasses_shape": {
                      "value": 0
                    },
                    "glasses_size": {
                      "value": 0
                    },
                    "glasses_thick": {
                      "value": 0
                    },
                    "glasses_type": {
                      "value": 0
                    },
                    "left_eyelid": {
                      "value": 1
                    },
                    "mustache_length": {
                      "value": 0
                    },
                    "mustache_shape": {
                      "value": 0
                    },
                    "mustache_thick": {
                      "value": 0
                    },
                    "mustache_type": {
                      "value": 0
                    },
                    "pitch_angle": {
                      "value": -2.675837516784668
                    },
                    "race": {
                      "value": 1
                    },
                    "right_eyelid": {
                      "value": 2
                    },
                    "roll_angle": {
                      "value": -7.028405666351318
                    },
                    "translate_x": {
                      "value": -118.0142822265625
                    },
                    "translate_y": {
                      "value": -418.3360595703125
                    },
                    "translate_z": {
                      "value": 2067.922119140625
                    },
                    "yaw_angle": {
                      "value": -27.13144302368164
                    }
                  },
                  "face_rectangle": {
                    "height": 0.052405804,
                    "left": 0.37931174,
                    "top": 0.26455134,
                    "width": 0.069874406
                  }
                }
              ]
            },
            "media_profiles": null
          }
        ],
        "msg_id": "",
        "parameter": {
          "landmark_type": 0,
          "version": "2.0.0"
        }
      },
      "media_info_list": [
        {
          "media_data": "",
          "media_extra": {
            "faces": [
              {
                "face_attributes": {
                  "age": {
                    "value": 17
                  },
                  "beauty_score": {
                    "value": 87
                  },
                  "emotion": {
                    "value": 1
                  },
                  "gender": {
                    "value": 0
                  },
                  "glasses_frame": {
                    "value": 0
                  },
                  "glasses_shape": {
                    "value": 0
                  },
                  "glasses_size": {
                    "value": 0
                  },
                  "glasses_thick": {
                    "value": 0
                  },
                  "glasses_type": {
                    "value": 0
                  },
                  "left_eyelid": {
                    "value": 1
                  },
                  "mustache_length": {
                    "value": 0
                  },
                  "mustache_shape": {
                    "value": 0
                  },
                  "mustache_thick": {
                    "value": 0
                  },
                  "mustache_type": {
                    "value": 0
                  },
                  "pitch_angle": {
                    "value": -2.675837516784668
                  },
                  "race": {
                    "value": 1
                  },
                  "right_eyelid": {
                    "value": 2
                  },
                  "roll_angle": {
                    "value": -7.028405666351318
                  },
                  "translate_x": {
                    "value": -118.0142822265625
                  },
                  "translate_y": {
                    "value": -418.3360595703125
                  },
                  "translate_z": {
                    "value": 2067.922119140625
                  },
                  "yaw_angle": {
                    "value": -27.13144302368164
                  }
                },
                "face_rectangle": {
                  "height": 0.052405804,
                  "left": 0.37931174,
                  "top": 0.26455134,
                  "width": 0.069874406
                }
              }
            ]
          },
          "media_profiles": null
        }
      ]
    },
    "progress": 0,
    "predict_elapsed": 10000,
    "create_time": 1770290367382,
    "task_id": "t_mt1a3i5n7b2e53ffab-xxxx-4802-a5ff-dc812744ffa7",
    "custom_task_id": "",
    "trace_id": "",
    "client_info": "",
    "init_images": null
  }
}

Failed Response Example

Response Status: 400; Content-Type: application/json; charset=utf-8

{
  "request_id": "",
  "trace_id": "",
  "code": 0,
  "error_code": 20001,
  "message": "PROCESS_ERROR",
  "tips": null,
  "data": {
    "status": 2,
    "result": {
      "error_code": 20001,
      "error_msg": "PROCESS_ERROR"
    },
    "progress": 0,
    "predict_elapsed": 0,
    "create_time": 1770290367382,
    "task_id": "t_mt1a3i5n7b2e53ffab-xxxx-4802-a5ff-dc812744ffa7",
    "custom_task_id": "",
    "trace_id": "",
    "client_info": "",
    "init_images": null
  }
}

General Error Codes and Messages

See API Error Codes.

SDK Examples

Python

import json

import requests
from sign_sdk import sign

def api_call_example():
    key = "your_api_key"
    secret = "your_api_secret"
    url = "https://openapi.meitu.com/api/v1/sdk/sync/push"
    method = "POST"
    headers = {
        "Content-Type": "application/json",
        sign.HeaderHost: "openapi.meitu.com",
    }
    inner_params = {
        "rsp_media_type": "url",
        "parameter": {
            "return_attributes": "gender,age,race,beauty,glasses,mustache,emotion,eyelid,poseestimation",
            "return_landmark": 0,
        },
    }
    payload = {
        "task": "/v1/Macro_facial_analysis/487163",
        "task_type": "formula",
        "init_images": [
            {
                "url": "https://example.com/input.jpg",
                "profile": {
                    "media_profiles": {"media_data_type": "url"},
                    "version": "v1",
                },
            }
        ],
        "params": json.dumps(inner_params, ensure_ascii=False),
        "sync_timeout": 30,
    }
    body = json.dumps(payload, ensure_ascii=False)

    signer = sign.Signer(key, secret)
    signed_request = signer.sign(url, method, headers, body)
    response = requests.Session().send(signed_request)
    print(f"Status: {response.status_code}")
    print(f"Response: {response.text}")

if __name__ == "__main__":
    api_call_example()

Go

package main

import (
	"fmt"
	"io"
	"net/http"

	"github.com/mtlab/api/signer"
)

func main() {
	key := "your_api_key"
	secret := "your_api_secret"
	signObj := signer.NewSigner(key, secret)

	url := "https://openapi.meitu.com/api/v1/sdk/sync/push"
	method := http.MethodPost
	headers := make(http.Header)
	headers.Set(signer.HeaderHost, "openapi.meitu.com")
	headers.Set("Content-Type", "application/json")

	body := `{
  "task": "/v1/Macro_facial_analysis/487163",
  "task_type": "formula",
  "init_images": [{
    "url": "https://example.com/input.jpg",
    "profile": {
      "media_profiles": {"media_data_type": "url"},
      "version": "v1"
    }
  }],
  "params": "{\"rsp_media_type\":\"url\",\"parameter\":{\"return_attributes\":\"gender,age,race,beauty,glasses,mustache,emotion,eyelid,poseestimation\",\"return_landmark\":0}}",
  "sync_timeout": 30
}`

	req, err := signObj.Sign(url, method, headers, body)
	if err != nil {
		fmt.Println("Failed to sign request:", err)
		return
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("Failed to send request:", err)
		return
	}
	defer resp.Body.Close()
	responseBody, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Read response failed:", err)
		return
	}
	fmt.Println("Response:", resp.StatusCode, string(responseBody))
}

PHP

<?php
require 'signer.php';

$key = 'your_api_key';
$secret = 'your_api_secret';
$signer = new Signer($key, $secret);

$url = 'https://openapi.meitu.com/api/v1/sdk/sync/push';
$method = 'POST';
$headers = ['Content-Type' => 'application/json'];

$innerParams = json_encode([
    'rsp_media_type' => 'url',
    'parameter' => [
        'return_attributes' => 'gender,age,race,beauty,glasses,mustache,emotion,eyelid,poseestimation',
        'return_landmark' => 0,
    ],
]);

$body = json_encode([
    'task' => '/v1/Macro_facial_analysis/487163',
    'task_type' => 'formula',
    'init_images' => [
        [
            'url' => 'https://example.com/input.jpg',
            'profile' => [
                'media_profiles' => ['media_data_type' => 'url'],
                'version' => 'v1',
            ],
        ],
    ],
    'params' => $innerParams,
    'sync_timeout' => 30,
]);

$curl = $signer->sign($url, $method, $headers, $body);
$response = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($status === 0) {
    echo 'Error: ' . curl_error($curl);
} else {
    echo "Status: {$status}\n";
    echo "Response: {$response}\n";
}
curl_close($curl);
?>

Java

package com.meitu.openai.common;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws Exception {
        Signer signer = new Signer("your_api_key", "your_api_secret");
        String url = "https://openapi.meitu.com/api/v1/sdk/sync/push";
        String method = "POST";

        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/json");
        headers.put(Signer.HeaderHost, "openapi.meitu.com");

        String body = "{\n" +
                "  \"task\": \"/v1/Macro_facial_analysis/487163\",\n" +
                "  \"task_type\": \"formula\",\n" +
                "  \"init_images\": [{\n" +
                "    \"url\": \"https://example.com/input.jpg\",\n" +
                "    \"profile\": {\n" +
                "      \"media_profiles\": {\"media_data_type\": \"url\"},\n" +
                "      \"version\": \"v1\"\n" +
                "    }\n" +
                "  }],\n" +
                "  \"params\": \"{\\\"rsp_media_type\\\":\\\"url\\\",\\\"parameter\\\":{\\\"return_attributes\\\":\\\"gender,age,race,beauty,glasses,mustache,emotion,eyelid,poseestimation\\\",\\\"return_landmark\\\":0}}\",\n" +
                "  \"sync_timeout\": 30\n" +
                "}";

        Map<String, String> signedHeaders = signer.sign(url, method, headers, body);
        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
        connection.setRequestMethod(method);
        for (Map.Entry<String, String> entry : signedHeaders.entrySet()) {
            connection.setRequestProperty(entry.getKey(), entry.getValue());
        }
        connection.setDoOutput(true);
        connection.getOutputStream().write(body.getBytes(StandardCharsets.UTF_8));

        int status = connection.getResponseCode();
        InputStream stream = status >= 400
                ? connection.getErrorStream()
                : connection.getInputStream();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            System.out.println("Response: " + status + " " + response);
        }
    }
}