> This page is for Applications Protection.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.humansecurity.com/llms.txt.

# Advanced Configuration

### Customizing Default Block Pages

**Custom Logo**\
It is possible to add a custom logo to the blocking page by adding the key `custom_logo` with a valid URL as the value to the `$perimeterxConfig` array. The logo will be displayed at the top div of the the block page. The logo's `max-height` property would be `150px` and width would be set to `auto`.

```php
$perimeterxConfig = [
  ...
  'custom_logo' => 'https://s.perimeterx.net/logo.png',
  ...
];
```

**Custom JS/CSS**

The block page can be modified with custom CSS by adding to the `$perimeterxConfig` the key `css_ref` and providing a valid URL to the CSS file. There is also the option to add a custom JS file by adding `js_ref` key to the `$perimeterxConfig` and providing the JS file that will be loaded with the block page. Both these configurations expect valid URLs. If the URLs provided are not valid, an exception will be thrown.

```php
$perimeterxConfig = [
  ...
  'css_ref' => 'https://s.perimeterx.net/custom_block.css',
  'js_ref' => 'https://s.perimeterx.net/custom_block.js',
  ...
];
```

**No Blocking, Monitor Only**

```php
/**
 * @param \Perimeterx\PerimeterxContext $pxCtx
 */
$perimeterxConfig['custom_block_handler'] = function ($pxCtx) {
    $block_score = $pxCtx->getScore();
    $block_uuid = $pxCtx->getUuid();
    $full_url = $pxCtx->getFullUrl();

    // user defined logic goes here
};

$px = Perimeterx::Instance($perimeterxConfig);
$px->pxVerify();
```

**Extracting Recommended Action**

```php
/**
 * @param \Perimeterx\PerimeterxContext $pxCtx
 */
$perimeterxConfig['custom_block_handler'] = function ($pxCtx) {
    $block_score = $pxCtx->getScore();
    $block_uuid = $pxCtx->getUuid();
    $action = $pxCtx->getBlockAction();

    /* user defined logic comes here */
    error_log('px score for user is ' . $block_score);
    error_log('px recommended action for user is ' . $action);
    error_log('px page uuid is ' . $block_uuid);

    switch ($action) {
        case "block":
            log("do block");
            break;
        case "captcha":
            log("do captcha");
            break;
        default:
            log("unknown action");
    }
};
```

### Module Mode

**Default mode:** `Perimeterx::$MONITOR_MODE`

**Possible Values:**

* `Perimeterx::$ACTIVE_MODE` - Module blocks users crossing the predefined block threshold. Server-to-server requests are sent synchronously.
* `Perimeterx::$MONITOR_MODE` - Module does not block users crossing the predefined block threshold. The pxCustomBlockHandler function will be eval'd in case one is supplied, upon crossing the defined block threshold.

```php
$perimeterxConfig = [
  ...
  'module_mode' => Perimeterx::$ACTIVE_MODE
  ...
];
```

### Extracting the Real User IP Address

> **Note**
>
> IP extraction, according to your network setup, is very important. It is common to have a load balancer or proxy in front of your applications, in which case the HUMAN module will send the system's internal IP as the user's. In order to properly perform processing and detection on server-to-server calls, the HUMAN module needs the real user's IP.

The user's IP can be passed to the HUMAN module using a custom user-defined function on the \$HUMANConfig array, or by passing a list of headers to extract the real IP from, ordered by priority.

**Default with no predefined header:** `$_SERVER['REMOTE_ADDR']`

```php
// IP extraction using request headers
$perimeterxConfig = [
  ...
  'ip_headers' => ['X-TRUE-IP', 'X-Forwarded-For']
  ...
];

/**
 * IP extraction using a custom user-defined function
 * @param \Perimeterx\PerimeterxContext $pxCtx
 */
$perimeterxConfig['custom_user_ip'] = function ($pxCtx) {
    $headers = getallheaders();

    /* using a socket ip */
    $ip = $_SERVER['REMOTE_ADDR'];

    /* using an ip from a x-forwarded-for header */
    $xff = explode(",", $headers['X-Forwarded-For']);
    $ip = $xff[count($xff)-1];

    /* using an ip from a custom header */
    $ip = $headers['X-REAL-CLIENT-IP'];

    return $ip;
};

$px = Perimeterx::Instance($perimeterxConfig);
$px->pxVerify();
```

\


### Custom URI

The URI can be returned to the HUMAN module using a custom user function defined on the \$HUMANConfig variable.

**Default:** `$_SERVER['REQUEST_URI']`

```php
/**
 * @param \Perimeterx\PerimeterxContext $pxCtx
 */
$perimeterxConfig['custom_uri'] = function ($pxCtx) {
    return $_SERVER['HTTP_X_CUSTOM_URI'];
};

$px = Perimeterx::Instance($perimeterxConfig);
$px->pxVerify();
```

### Filter Sensitive Headers

A list of sensitive headers can be configured to prevent specific headers from being sent to HUMAN servers (lower case header names). Filtering cookie headers for privacy is set by default, and can be overridden on the \$HUMANConfig variable.

**Default: cookie, cookies**

```php
$perimeterxConfig = [
  ...
  'sensitive_headers' => ['cookie', 'cookies', 'secret-header']
  ...
];
```

### Sensitive Routes

List of route prefixes. The HUMAN module will always match request URIs by this prefix lis. If match is found, the module will make a server-to-server call for the request, even if the cookie score is low and valid.

**Default: None**

```php
$perimeterxConfig = [
  ...
  'sensitive_routes' => ['/login', '/user/profile']
  ...
];
```

\


### API Timeouts

The timeout, in seconds (float), to wait for the HUMAN server API response. This controls the timeouts for HUMAN requests. The server-to-server API call is made when a risk cookie does not exist, or is expired or invalid. If the timeout is reached before the module receives a response, the client request will be passed.

**Default:** 1

```php
$perimeterxConfig = [
  ...
  'api_timeout' => 2
  ...
];
```

### API Connection Timeout

The API connection timeout, in seconds (float), is the amount of time to wait for the connection to the HUMAN server API.

**Default:** 1

```php
$perimeterxConfig = [
  ...
  'api_connect_timeout' => 2
  ...
];
```

### Activities Timeout

The activities timeout is the amount of time in seconds (float) to wait for the HUMAN server response.

**Default:** 1

```php
$perimeterxConfig = [
  ...
  'activities_timeout' => 2
  ...
];
```

### Activities Connection Timeout

The activities connection timeout is the amount of time in seconds (float) to wait for the connection to the HUMAN server when sending non-risk\_api activities.

**Default:** 1

```php
$perimeterxConfig = [
  ...
  'activities_connect_timeout' => 2
  ...
];
```

### First Party

A boolean flag to enable/disable first party mode.

**Default:** true

```php
$perimeterxConfig = [
    ..
    'px_first_party_enabled' => false
    ..
]
```

### First Party for Code Defender

A boolean flag to enable/disable first party mode for Code Defender.

**Default:** false

```php
$perimeterxConfig = [
    ..
    'px_cd_first_party_enabled' => true
    ..
]
```

### Send Page Activities

A boolean flag to enable or disable sending of activities and metrics to HUMAN on each page request. Enabling this feature provides data that populates the HUMAN portal with valuable information, such as the amount of requests blocked and additional API usage statistics.

**Default:** false

```php
$perimeterxConfig = [
  ...
  'send_page_activities' => true
  ...
];
```

### Additional Page Activity Handler

Adding an additional activity handler is done by setting 'additional\_activity\_handler' with a user defined function on the '\$HUMANConfig' variable. The 'additional\_activity\_handler' function will be executed before sending the data to the HUMAN portal.

**Default:** Only send activity to HUMAN as controlled by '\$HUMANConfig'.

```php
/**
 * @param string            $activityType
 * @param PerimeterxContext $pxCtx
 * @param array             $details
 */
$perimeterxConfig['additional_activity_handler'] = function ($activityType, $pxCtx, $details)
{
    // user defined logic here
};

$px = Perimeterx::Instance($perimeterxConfig);
$px->pxVerify();
```

#### Additional Activity Handler Usage Examples

**Log Activity**

```php
/**
 * @param string            $activityType
 * @param PerimeterxContext $pxCtx
 * @param array             $details
 */
$perimeterxConfig['additional_activity_handler'] = function ($activityType, $pxCtx, $details) use ($logger)
{
    if ($activityType === 'block') {
        $logger->warning('PerimeterX {activityType} details', ['activityType' => $activityType, 'details' => $details]);
    } else {
        $logger->info('PerimeterX {activityType} details', ['activityType' => $activityType, 'details' => $details]);
    }
};

$px = Perimeterx::Instance($perimeterxConfig);
$px->pxVerify();
```

**Send Activity to statsd**

```php
/**
 * @param string            $activityType
 * @param PerimeterxContext $pxCtx
 * @param array             $details
 */
$perimeterxConfig['additional_activity_handler'] = function ($activityType, $pxCtx, $details) use ($statsd)
{
    $statsd->increment('perimeterx_activity.' . $activityType);
};

$px = Perimeterx::Instance($perimeterxConfig);
$px->pxVerify();
```

### Data-Enrichment

The additional activity handler can be used to retrieve information about the request using the data-enrichment object. First, validate the data enrichment object is verified, then you can access its properties.

**Default:** false

```php
/**
 * @param string            $activityType
 * @param PerimeterxContext $pxCtx
 * @param array             $details
 */
$perimeterxConfig['additional_activity_handler'] = function ($activityType, $pxCtx, $details) use ($logger)
{
    if($pxCtx->getDataEnrichmentVerified()) {
        $pxde = $pxCtx->getDataEnrichment();
        if($pxde->f_type == 'blacklist') {
            $logger->info('Filtered request with id: {$pxde->f_id} at: {$pxde->timestamp}');
        }
    }
};

$px = Perimeterx::Instance($perimeterxConfig);
$px->pxVerify();
```

### Enrich Custom Params

With the `enrich_custom_params` function, you can add up to 10 custom parameters to be sent back to HUMAN servers. When set, the function is called before setting the payload on every request to HUMAN servers. The parameters should be passed according to the correct order (1-10).

**Default:** not set

```php
/**
 * @param array             $customParamsArray
 */
$perimeterxConfig['enrich_custom_params'] = function ($customParamsArray)
{
    // user defined logic comes here
};

$px = Perimeterx::Instance($perimeterxConfig);
$px->pxVerify();
```

##### Enrich Custom Params Usage Examples

```php
/**
 * @param array             $customParamsArray
 */
$perimeterxConfig['enrich_custom_params'] = function ($customParamsArray)
{
    $customParamsArray['custom_param1'] = "UserId";
    $customParamsArray['custom_param2'] = "SessionId";
    return $customParamsArray;
};
```

### Logging

The HUMAN module logs messages via an implementation of `\Psr\Log\LoggerInterface` (see [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) for full interface specification). By default, an instance of `\Perimeterx\PerimeterxLogger` is used which will log all message via PHP's `error_log` function. However, any custom logger that implements this interface may be used instead.

**Default:** `\Perimeterx\PerimeterxLogger` instance

```php
$perimeterxConfig = [
  ...
  'logger' => new \My\Psr\Log\ConcreteLogger()
  ...
];
```

### Debug Mode

Enables debug logging mode.

**Default:** false

```php
$perimeterxConfig = [
  ...
  'debug_mode' => true
  ...
];
```

Once enabled, debug messages coming out from HUMAN should be in the following template:

`[PerimeterX - DEBUG][APP_ID] - MESSAGE` - for debug messages

`[PerimeterX - ERROR][APP_ID] - MESSAGE` - for error messages

An example log for an high score cookie:

```none
[Mon Dec  4 14:03:50 20XX] [PerimeterX - DEBUG][APP_ID] -Starting request verification
[Mon Dec  4 14:03:50 20XX] [PerimeterX - DEBUG][APP_ID] -Request context created successfully
[Mon Dec  4 14:03:50 20XX] [PerimeterX - DEBUG][APP_ID] -No Captcha cookie present on the request
[Mon Dec  4 14:03:50 20XX] [PerimeterX - DEBUG][APP_ID] -Cookie V3 found, Evaluating
[Mon Dec  4 14:03:50 20XX] [PerimeterX - DEBUG][APP_ID] -Cookie evaluation ended successfully, risk score: 100
[Mon Dec  4 14:03:51 20XX] [PerimeterX - DEBUG][APP_ID] -Enforcing action: Captcha page is served
```

\


### Guzzle Client Handler

Allows setting a handler to the Guzzle client object.

**Default:** false

```php
$container = [];
$history = Middleware::history($container);
$handler = HandlerStack::create();
$handler->push($history);


$perimeterxConfig = [
  ...
  'guzzle_handler' => $handler
  ...
];
```

### Custom Block URL

You can customize the block page to meet branding and message requirements by specifying the URL of the block page HTML file. The module will redirect to the block page defined in the `custom_block_url` variable. The defined block page will display a 307 (Temporary Redirect) HTTP Response Code.

**Default:** not set

```php
$perimeterxConfig = [
  ...
  'custom_block_url' => '/block.html'
  ...
];
```

### Defer Activities Sending

Specifies if sending page activities should be deferred until shutdown or not.

**Default:** true

```php
$perimeterxConfig = [
  ...
  'defer_activities' => false
  ...
];
```

### Test Block Flow on Monitoring Mode

Allows you to test an enforcer’s blocking flow while you are still in monitor mode.

When the header name is set (e.g., `x-px-block`) and the value is set to `1`, when there is a block response (for example from using a User-Agent header with the value of `PhantomJS/1.0`) the module's monitor mode is bypassed and active blocking mode is applied. If one of the conditions is missing, you will stay in monitor mode. This is done per request. To stay in monitor mode, set the header value to `0`.

The header name is configurable using the `bypass_monitor_header` property.

**Default:** not set

```php
$perimeterxConfig = [
  ...
  'bypass_monitor_header' => 'x-px-block'
  ...
];
```

### Return Response

Enables/disables the ability to return the response back (useful for frameworks like Symfony) instead of running `die()`.

**Default:** false

```php
$perimeterxConfig = [
  ...
  'return_response' => true
  ...
];
```

### Advanced Blocking Response

Enables/disables the Advanced Blocking Response functionality, which returns the block response as a JSON object instead of HTML.

**Default:** false

```php
$perimeterxConfig = [
  ...
  'enable_json_response' => true
  ...
];
```

### Login Credentials Extraction

This feature extracts credentials (hashed username and password) from requests and sends them to HUMAN as additional info in the risk api call. The feature can be toggled on and off, and may be set for any number of unique paths. The settings are adjusted by modifying the `px_login_credentials_extraction_enabled` and `px_login_credentials_extraction` properties on the `$perimeterxConfig` array.

If credentials are found to be compromised, the field `px-compromised-credentials` will be added to the `$_REQUEST` object with the value `"1"`. You may configure the name of this field with the `px_compromised_credentials_header` configuration.

**Default Values**

px\_login\_credentials\_extraction\_enabled: false

px\_login\_credentials\_extraction: \[]

px\_compromised\_credentials\_header: "px-compromised-credentials"

```php
$perimeterxConfig['px_compromised_credentials_header'] = 'px-comp-creds';
$perimeterxConfig['px_login_credentials_extraction_enabled'] = true;
$perimeterxConfig['px_login_credentials_extraction'] = [
    [
        "path" => "/login",           // login path, automatically added to sensitive routes
        "method" => "POST",           // supported methods: POST
        "sent_through" => "body",     // supported sent_throughs: body, header, query-param
        "pass_field" => "password",   // name of the password field in the request
        "user_field" => "username"    // name of the username field in the request
    ], [ ... ], ...
]
```

It is also possible to define a custom callback to extract the username and password. The function should return an associative array with the keys `user` and `pass`. If extraction is unsuccessful, the function should return `null`.

```php
$perimeterxConfig['px_login_credentials_extraction_enabled'] = true;
$perimeterxConfig['px_login_credentials_extraction'] = [
    [
        "path" => "/login",                 // login path
        "method" => "POST",                 // supported methods: POST
        "callback_name" => "extractCreds"   // name of custom extraction callback
    ], ...
];

function extractCreds() {
    // custom implementation resulting in $username and $password
    if (empty($username) || empty($password)) {
        return null;
    }
    return [
        "user" => $username,
        "pass" => $password
    ];
}
```

### JWT User Identifiers

This feature extracts user identity information from a JWT (JSON Web Token) present in a cookie or header and sends it to HUMAN as part of the risk evaluation and activity data.

The extracted data includes:

* **App User ID** - A single field representing the user's identity
* **Additional Fields** - Optional extra fields from the JWT payload

The enforcer also automatically extracts the Cross-Tab Session (CTS) value from the `pxcts` cookie if present, which is sent to HUMAN for enhanced session tracking.

> **Note**
>
> If both cookie and header JWT configurations are provided, the cookie takes precedence over the header.

**Default Values**

px\_jwt\_cookie\_name: null (disabled)

px\_jwt\_cookie\_user\_id\_field\_name: null

px\_jwt\_cookie\_additional\_field\_names: \[]

px\_jwt\_header\_name: null (disabled)

px\_jwt\_header\_user\_id\_field\_name: null

px\_jwt\_header\_additional\_field\_names: \[]

```php
$perimeterxConfig = [
    // Extract JWT from a cookie
    'px_jwt_cookie_name' => 'auth_token',
    'px_jwt_cookie_user_id_field_name' => 'sub',
    'px_jwt_cookie_additional_field_names' => ['session.id', 'account.type'],

    // Or extract JWT from a header (cookie takes precedence if both configured)
    'px_jwt_header_name' => 'Authorization',
    'px_jwt_header_user_id_field_name' => 'user.id',
    'px_jwt_header_additional_field_names' => ['tenant.id']
];
```

#### Configuration Options

| Option                                 | Type   | Description                                                             |
| -------------------------------------- | ------ | ----------------------------------------------------------------------- |
| `px_jwt_cookie_name`                   | string | Name of the cookie containing the JWT token                             |
| `px_jwt_cookie_user_id_field_name`     | string | Dot-notated path to the user ID field in the JWT payload                |
| `px_jwt_cookie_additional_field_names` | array  | List of dot-notated paths for additional fields to extract from the JWT |
| `px_jwt_header_name`                   | string | Name of the header containing the JWT token                             |
| `px_jwt_header_user_id_field_name`     | string | Dot-notated path to the user ID field in the JWT payload                |
| `px_jwt_header_additional_field_names` | array  | List of dot-notated paths for additional fields to extract from the JWT |

#### Nested Fields

Use dot notation to access nested fields in the JWT payload. For example, given a JWT with the following payload:

```json
{
  "user": {
    "id": "123456"
  },
  "session": {
    "id": "abc-def"
  }
}
```

Configure the extraction like this:

```php
$perimeterxConfig = [
    'px_jwt_cookie_name' => 'auth_token',
    'px_jwt_cookie_user_id_field_name' => 'user.id',
    'px_jwt_cookie_additional_field_names' => ['session.id']
];
```

The extracted values will be sent to HUMAN as:

* `app_user_id`: `"123456"`
* `jwt_additional_fields`: `{"session.id": "abc-def"}`

### Additional S2S Activity

To enhance detection on login credentials extraction endpoints, the following additional information is sent to HUMAN via an `additional_s2s` activity:

* **Response Code** - The numerical HTTP status code of the response. This is sent automatically.
* **Login Success** - A boolean indicating whether the login completed successfully. See the options listed below for how to provide this data.
* **Raw Username** - The original username used for the login attempt. In order to report this information, make sure the configuration `px_send_raw_username_on_additional_s2s_activity` is set to `true`.

#### Login Success Reporting

There are a number of different possible ways to report the success or failure of the login attempt. If left empty, the login successful status will always be reported as `false`.

**Default**: Empty

```php
$perimeterxConfig = [
  ...
  'px_login_successful_reporting_method' => 'status'
  ...
];
```

**Status**

Provide a status or array of statuses that represent a successful login. If a response's status code matches the provided\
value or one of the values in the provided array, the login successful status is set to `true`. Otherwise, it's set to `false`.

> **Note**
>
> To define a range of statuses, use the `custom` reporting method.

**Default Values**

px\_login\_successful\_status: 200

```php
$perimeterxConfig['px_login_successful_reporting_method'] = 'status';
$perimeterxConfig['px_login_successful_status'] = [200, 202]; // number or array of numbers
```

**Header**

Provide a header name and value. If the header exists on the response (accessed via the `headers_list()` function) and matches the provided value, the login successful status is set to `true`. If the header is not found on the response, or if the header value does not match the value in the configuration, the login successful status is set to `false`.

**Default Values**

px\_login\_successful\_header\_name: x-px-login-successful

px\_login\_successful\_header\_value: 1

```php
$perimeterxConfig['px_login_successful_reporting_method'] = 'header';
$perimeterxConfig['px_login_successful_header_name'] = 'login-successful';
$perimeterxConfig['px_login_successful_header_value'] = 'true';
```

**Custom**

Provide a custom callback that returns a boolean indicating if the login was successful. The value of the configuration field can be either an anonymous function or the name of the defined function as a string.

**Default Values**

px\_login\_successful\_custom\_callback: null

```php
$perimeterxConfig['px_login_successful_reporting_method'] = 'custom';

// anonymous callback function
$perimeterxConfig['px_login_successful_custom_callback'] = function() {
    // ...
    return $isLoginSuccessful;
};

// name of defined function as string
$perimeterxConfig['px_login_successful_custom_callback'] = 'isLoginSuccessfulCallback';

function isLoginSuccessfulCallback() {
    // ...
    return $isLoginSuccessful;
}
```

### Raw Username

When enabled, the raw username used for logins on login credentials extraction endpoints will be reported to HUMAN if (1) the credentials were identified as compromised, and (2) the login was successful as reported via the property above.

**Default**: false

```php
$perimeterxConfig['px_send_raw_username_on_additional_s2s_activity'] = true;
```

### Manually Sending Additional S2S Activity

By default, this `additional_s2s` activity is sent automatically. If it is preferable to send this activity manually,\
it's possible to disable automatic sending by configuring the value of `px_automatic_additional_s2s_activity_enabled` to `false`.

**Default Value**: true

```php
$perimeterxConfig['px_automatic_additional_s2s_activity_enabled'] = false;
```

\


The activity can then be sent manually by invoking the function `$px->pxSendAdditionalS2SActivity()`, which accepts the following parameters:

| **Parameter Name**        | **Type**  | **Description**                                                                                                                                                  |
| ------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`$responseStatusCode`** | `integer` | The HTTP status code of the response, typically used to determine if the login was successful (e.g., 200 for success).                                           |
| **`$wasLoginSuccessful`** | `boolean` | A boolean flag indicating if the login attempt was successful. This is generally determined by evaluating the response status code or other validation criteria. |

**Example Usage**

```php
// $px is an instance of the Perimeterx class

function handleLogin() {
    // login flow resulting in boolean $isLoginSuccessful
    $px->pxSendAdditionalS2SActivity($isLoginSuccessful ? 200 : 401, $isLoginSuccessful);
}
```

If further flexibility is needed, a JSON representation of the `additional_s2s` activity can be added to the `$_REQUEST` array. This activity can then be sent to another server if needed, parsed, modified, and sent via XHR POST as a JSON to HUMAN. To do this, disable automatic sending and enable the additional activity header configuration.

**Default Value**

px\_additional\_s2s\_activity\_header\_enabled: false

```php
$perimeterxConfig['px_automatic_additional_s2s_activity_enabled'] = false;
$perimeterxConfig['px_additional_s2s_activity_header_enabled'] = true;
```

The activity payload and URL destination will then be available by accessing `$_REQUEST['px-additional-activity']` and `$_REQUEST['px-additional-activity-url']`, respectively.

```php
function handleLogin() {
    // custom flow resulting in boolean $isLoginSuccessful
    $activity = json_decode($_REQUEST['px-additional-activity'], true);
    $activity['additional']['http_status_code'] = http_status_code();
    $activity['additional']['login_successful'] = $isLoginSuccessful;

    if ($isLoginSuccessful && $activity['additional']['credentials_compromised']) {
        $activity['additional']['raw_username'] = $_REQUEST['username'];
    }

    $url = $_REQUEST['px-additional-activity-url'];
    $headers = [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $_ENV['PX_AUTH_TOKEN']
    ];
    $body = json_encode($activity);

    sendPostRequest($url, $headers, $body);
}

function sendPostRequest($url, $headers, $body) {
    $curl = curl_init($url);
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_POST, true);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
    curl_exec($curl);
}
```