protected function execCurl($ch)
    {
        $headers = [];
        $response = [];

        curl_setopt($ch, CURLOPT_HEADERFUNCTION,
          function($curl, $header) use (&$headers)
          {
              $len = strlen($header);
              $header = explode(':', $header, 2);
              if (count($header) < 2) {
                  return $len;
              }

              $name = strtolower(trim($header[0]));
              if (!array_key_exists($name, $headers)) {
                  $headers[$name] = [trim($header[1])];
              } else {
                  $headers[$name][] = trim($header[1]);
              }

            return $len;
          }
        );

        $data = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $body = empty($data) ? null : json_decode($data, true);

        $response = [
            'status' => $status,
            'headers' => $headers,
            'body' => $body
        ];

        // inform the user of a connection failure
        if ($status == 0 || $response['body'] === false) {
            throw new ConnectionException(curl_error($ch));
        }

        // or an error response from Chatkit
        if ($status >= 400) {
            $this->log('ERROR: execCurl error: '.print_r($response, true));
            throw (new ChatkitException($response['body']['error_description'], $status))->setBody($response['body']);
        }

        $this->log('INFO: execCurl response: '.print_r($response, true));
        return $response;
    }