HTTP_Request_Send PUT - Trouble understand format for Body

I am working on an integration from a ESP32 to SignalK with the goal of sending sensor values. SignalK requires you to login first then it returns a token which must be used for the PUT action. So far the login works fine and I can parse the Token out of the response.

Reviewing the documentation https://esphome.io/components/http_request/#http_requestpost-action it appears I need to use the HTTP_Request.Send action with a method of Put. What is not clear is how to format the JSON. The docs show

body: "Some data"
I used the JSON builder and it compiled but getting a 401 which is permission error. I need to send the following with the "value" and "token" being variables. The format was validated using Postman.

{
    "value": 1.0,
    "source": "esphome",
    "login": {
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVzcGhvbWUiLCJpYXQiOjE3NjI5NjQzNDV9.a8oGe1SeVdYFBkn9cTZh6eNA3f2DVMJAd9YDDyDOh_w"
    }
}

Here's my code;

esphome:
  name: forward-black-tank-monitor
  friendly_name: Forward Black Tank Monitor

esp32:
  board: esp32dev
  framework:
    type: esp-idf

# Enable logging
logger:
  level: DEBUG

# Enable Home Assistant API
api:
  encryption:
    key: "????"

ota:
  - platform: esphome
    password: "????"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Forward-Black-Tank-Monitor"
    password: "????"
  manual_ip:
    static_ip: !secret forward-black-tank-monitor
    gateway: !secret ip_gateway
    subnet: !secret ip_subnet

captive_portal:

http_request:
  id: http_request_data
  verify_ssl: false

text_sensor:
  - platform: template
    id: token
    name: "Token"
 
binary_sensor:
  - platform: template
    id: get_token
    trigger_on_initial_state: true
    internal: true
    on_state:
      then:
        - logger.log: "HTTP Login"
        - http_request.post:
            url: http://192.168.1.2:3000/signalk/v1/auth/login
            request_headers:
              Content-Length: 0
              Content-Type: application/json
            json: |-
              root["username"] = "esphome";
              root["password"] = "???";
            capture_response: true 
            on_response:
              then:
                - if:
                    condition:
                        lambda: return response->status_code == 200;
                    then:                
                      - lambda: |-
                          json::parse_json(body, [](JsonObject root) -> bool {
                              if (root["token"]) {
                                  id(token).publish_state(root["token"]);
                                  return true;
                              }
                              else {
                                return false;
                              }
                          });
                      - logger.log:
                          format: "Error: Response status: %d, message %s"
                          args: [ 'response->status_code', 'body.c_str()' ]     
                    else:
                      - logger.log:
                          format: "Error: Response status: %d, message %s"
                          args: [ 'response->status_code', 'body.c_str()' ]                         
#                     const char* httpResponse = id(http_request_data).c_str();                      
#                     json::parse_json(httpResponse), [](JsonObject root){id(token).publish_state(root["token"]);});    

  - platform: gpio
    pin:
      number: GPIO26
      mode:
        input: true
        pullup: true
    name: “tank_sensor_1”
    id: tank_sensor_1
    internal: true
  - platform: gpio
    pin:
      number: GPIO04
      mode:
        input: true
        pullup: true
    name: “tank_sensor_2”
    id: tank_sensor_2
    internal: true
  - platform: gpio
    pin:
      number: GPIO14
      mode:
        input: true
        pullup: true
    name: “tank_sensor_3”
    id: tank_sensor_3
    internal: true
  - platform: gpio
    pin:
      number: GPIO13
      mode:
        input: true
        pullup: true
    name: “tank_sensor_4”
    id: tank_sensor_4
    internal: true
  - platform: gpio
    pin:
      number: GPIO02
      mode:
        input: true
        pullup: true
    name: “tank_sensor_5”
    id: tank_sensor_5
    internal: true

sensor:
  - platform: template
    name: "Forward Blackwater Level"
    id: forward_blackwater_level
    lambda: |-
      if (id(tank_sensor_5).state) {
        return 1.0;
      } else {
      if (id(tank_sensor_4).state) {
        return 0.8;
      } else {  
      if (id(tank_sensor_3).state) {
        return 0.6;
      } else {  
      if (id(tank_sensor_2).state) {
        return 0.4;  
      } else {  
      if (id(tank_sensor_1).state) {
        return 0.2;  
      } else {  
        return 0.0;  
      }}}}}
    update_interval: 60s
    
    on_value:
      then:
        - http_request.send:
            method: PUT
            request_headers:
              Content-Length: 80
              Content-Type: application/json
            url: http://192.168.1.2:3000/signalk/v1/api/vessels/self/tanks/blackWater/fwd/currentLevel
            json: |-
              root["value"] = id(forward_blackwater_level).state;
              root["source"] = "esphome";
              root["login"] ["token"] = id(token).state;
            capture_response: true 
            on_response:
                - then:
                  - logger.log:
                      format: "Error: Response status: %d, message %s"
                      args: [ 'response->status_code', 'body.c_str()' ]    
                  - if:
                      condition:
                          lambda: |-
                              return response->status_code == 401;  
                      then:
                        - binary_sensor.template.publish:
                            id: get_token
                            state: false

According to the doc, the send method supports the same options as post, so json: instead of body: should work.

Where is that code failing?

As per the log below it is failing on permissions (401). All I can suspect is the JSON is not constructed correctly, but I am not sure how to correct it.

[20:25:15.542][D][main:366]: HTTP Login
[20:25:16.893][D][http_request.idf:043]: Received response header, name: content-type, value: application/json; charset=utf-8
[20:25:16.901][D][http_request.idf:043]: Received response header, name: content-length, value: 137
[20:25:16.901][D][text_sensor:085]: ‘Token’: Sending state ‘eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVzcGhvbWUiLCJpYXQiOjE3NjM0OTM5MTZ9.5xc7c5rGR17iVxD3DQ1J73Q4O1PwHpeG5GzkbbBXLoc’
[20:25:16.912][D][main:082]: Error: Response status: 200, message {“token”:“eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVzcGhvbWUiLCJpYXQiOjE3NjM0OTM5MTZ9.5xc7c5rGR17iVxD3DQ1J73Q4O1PwHpeG5GzkbbBXLoc”}
[20:25:16.917][D][binary_sensor:039]: ‘get_token’: New state is OFF
[20:25:16.918][W][component:453]: template.sensor took a long time for an operation (1481 ms)
[20:25:16.946][W][component:456]: Components should block for at most 30 ms
[20:25:16.946][W][component:453]: api took a long time for an operation (1495 ms)
[20:25:16.946][W][component:456]: Components should block for at most 30 ms
[20:25:16.947][E][component:342]: http_request cleared Error flag
[20:26:15.445][D][sensor:131]: ‘Forward Blackwater Level’: Sending state 0.00000 with 1 decimals of accuracy
[20:26:15.498][D][http_request.idf:043]: Received response header, name: content-type, value: text/plain; charset=utf-8
[20:26:15.499][D][http_request.idf:043]: Received response header, name: content-length, value: 91
[20:26:15.500][E][http_request.idf:207]: HTTP Request failed; URL: http://192.168.1.2:3000/signalk/v1/api/vessels/self/tanks/blackWater/fwd/currentLevel; Code: 401
[20:26:15.500][E][component:314]: http_request set Error flag: unspecified
[20:26:15.522][D][main:179]: Error: Response status: 401, message You do not have permission to view this resource, Please Login
[20:26:16.506][E][component:342]: http_request cleared Error flag

Are you sure the token is supposed to be in the body?
Typically, it would be in a header, in the form Authorization: bearer ey...

Do you have the doc of the API?

EDIT:
If this, the token should be in the root of the json request, not under “login”

I got distracted with a few other boat projects, but I am not back looking at this. I tried with your suggestion and that works with Postman as per the following screen shot.

Here’s my esphome YAML.

esphome:
  name: forward-black-tank-monitor
  friendly_name: Forward Black Tank Monitor

esp32:
  board: esp32dev
  framework:
    type: esp-idf

# Enable logging
logger:
  level: DEBUG

# Enable Home Assistant API
api:
  encryption:
    key: "ZJ7jwE4u2kWZZUkAeuP2zzJRszZvplycDjkwl6ksQ7Q="

ota:
  - platform: esphome
    password: "524bf492418369435fe1e37c02b25dbb"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Forward-Black-Tank-Monitor"
    password: "G5usbJhEKZgv"
  manual_ip:
    static_ip: !secret forward-black-tank-monitor
    gateway: !secret ip_gateway
    subnet: !secret ip_subnet

captive_portal:

http_request:
  id: http_request_data
  verify_ssl: false

text_sensor:
  - platform: template
    id: token
    name: "Token"
 
binary_sensor:
  - platform: template
    id: get_token
    trigger_on_initial_state: true
    internal: true
    on_state:
      then:
        - logger.log: "HTTP Login"
        - http_request.post:
            url: http://192.168.1.4:3000/signalk/v1/auth/login
            request_headers:
              Content-Length: 0
              Content-Type: application/json
            json: |-
              root["username"] = "esphome";
              root["password"] = "letmeinnow";
            capture_response: true 
            on_response:
              then:
                - if:
                    condition:
                        lambda: return response->status_code == 200;
                    then:                
                      - lambda: |-
                          json::parse_json(body, [](JsonObject root) -> bool {
                              if (root["token"]) {
                                  id(token).publish_state(root["token"]);
                                  return true;
                              }
                              else {
                                return false;
                              }
                          });
                      - logger.log:
                          format: "Error: Response status: %d, message %s"
                          args: [ 'response->status_code', 'body.c_str()' ]     
                    else:
                      - logger.log:
                          format: "Error: Response status: %d, message %s"
                          args: [ 'response->status_code', 'body.c_str()' ]                            

  - platform: gpio
    pin:
      number: GPIO26
      mode:
        input: true
        pullup: true
    name: “tank_sensor_1”
    id: tank_sensor_1
    internal: true
  - platform: gpio
    pin:
      number: GPIO04
      mode:
        input: true
        pullup: true
    name: “tank_sensor_2”
    id: tank_sensor_2
    internal: true
  - platform: gpio
    pin:
      number: GPIO14
      mode:
        input: true
        pullup: true
    name: “tank_sensor_3”
    id: tank_sensor_3
    internal: true
  - platform: gpio
    pin:
      number: GPIO13
      mode:
        input: true
        pullup: true
    name: “tank_sensor_4”
    id: tank_sensor_4
    internal: true
  - platform: gpio
    pin:
      number: GPIO02
      mode:
        input: true
        pullup: true
    name: “tank_sensor_5”
    id: tank_sensor_5
    internal: true

sensor:
  - platform: template
    name: "Forward Blackwater Level"
    id: forward_blackwater_level
    unit_of_measurement: '%'
    lambda: |-
      if (id(tank_sensor_5).state) {
        return 1.0;
      } else {
      if (id(tank_sensor_4).state) {
        return 0.8;
      } else {  
      if (id(tank_sensor_3).state) {
        return 0.6;
      } else {  
      if (id(tank_sensor_2).state) {
        return 0.4;  
      } else {  
      if (id(tank_sensor_1).state) {
        return 0.2;  
      } else {  
        return 0.0;  
      }}}}}
    update_interval: 15s
    
    on_value:
      then:
        - http_request.send:
            method: PUT
            url: http://192.168.1.4:3000/signalk/v1/api/vessels/self/tanks/blackWater/fwd/currentLevel
            request_headers:
              Content-Type: application/json
            json: |-
              root["source"] = "esphome";
              root["value"] = id(forward_blackwater_level).state;
              root["token"] = id(token).state;
            capture_response: true 
            on_response:
                - then:
                  - logger.log:
                      format: "Error: Response status: %d, message %s"
                      args: [ 'response->status_code', 'body.c_str()' ]    
                  - if:
                      condition:
                          lambda: |-
                              return response->status_code == 401;  
                      then:
                        - binary_sensor.template.publish:
                            id: get_token
                            state: false      
                          

But, still getting a permission error. As you can see the Post was successful and I am able to retrieve the Token.

[13:06:05.497][D][main:374]: HTTP Login
[13:06:05.752][D][http_request.idf:043]: Received response header, name: content-type, value: application/json; charset=utf-8
[13:06:05.752][D][http_request.idf:043]: Received response header, name: content-length, value: 137
[13:06:05.752][D][text_sensor:087]: ‘Token’: Sending state ‘eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVzcGhvbWUiLCJpYXQiOjE3NjQyNDUxNjV9.GeeqyZuDp1BaqNl7tqi7NxzV1QgA-nNGi5dFs2OqTZ8’
[13:06:05.753][D][main:082]: Error: Response status: 200, message {“token”:“eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVzcGhvbWUiLCJpYXQiOjE3NjQyNDUxNjV9.GeeqyZuDp1BaqNl7tqi7NxzV1QgA-nNGi5dFs2OqTZ8”}
[13:06:05.762][D][binary_sensor:041]: ‘get_token’: New state is OFF
[13:06:05.781][W][component:454]: template.sensor took a long time for an operation (347 ms)
[13:06:05.804][W][component:457]: Components should block for at most 30 ms
[13:06:05.806][W][component:454]: api took a long time for an operation (377 ms)
[13:06:05.806][W][component:457]: Components should block for at most 30 ms
[13:06:06.478][E][component:343]: http_request cleared Error flag
[13:06:20.424][D][sensor:133]: ‘Forward Blackwater Level’: Sending state 0.00000 % with 1 decimals of accuracy
[13:06:20.458][D][http_request.idf:043]: Received response header, name: content-type, value: text/plain; charset=utf-8
[13:06:20.458][D][http_request.idf:043]: Received response header, name: content-length, value: 91
[13:06:20.458][E][http_request.idf:208]: HTTP Request failed; URL: http://192.168.1.4:3000/signalk/v1/api/vessels/self/tanks/blackWater/fwd/currentLevel; Code: 401
[13:06:20.458][E][component:315]: http_request set Error flag: unspecified
[13:06:20.473][D][main:174]: Error: Response status: 401, message You do not have permission to view this resource, Please Login
[13:06:21.462][E][component:343]: http_request cleared Error flag
[13:06:35.430][D][sensor:133]: ‘Forward Blackwater Level’: Sending state 0.00000 % with 1 decimals of accuracy
[13:06:35.461][D][http_request.idf:043]: Received response header, name: content-type, value: text/plain; charset=utf-8
[13:06:35.469][D][http_request.idf:043]: Received response header, name: content-length, value: 91
[13:06:35.469][E][http_request.idf:208]: HTTP Request failed; URL: http://192.168.1.4:3000/signalk/v1/api/vessels/self/tanks/blackWater/fwd/currentLevel; Code: 401
[13:06:35.469][E][component:315]: http_request set Error flag: unspecified
[13:06:35.469][D][main:174]: Error: Response status: 401, message You do not have permission to view this resource, Please Login
[13:06:36.463][E][component:343]: http_request cleared Error flag
[13:06:50.434][D][sensor:133]: ‘Forward Blackwater Level’: Sending state 0.00000 % with 1 decimals of accuracy
[13:06:50.463][D][http_request.idf:043]: Received response header, name: content-type, value: text/plain; charset=utf-8
[13:06:50.464][D][http_request.idf:043]: Received response header, name: content-length, value: 91
[13:06:50.464][E][http_request.idf:208]: HTTP Request failed; URL: http://192.168.1.4:3000/signalk/v1/api/vessels/self/tanks/blackWater/fwd/currentLevel; Code: 401
[13:06:50.472][E][component:315]: http_request set Error flag: unspecified
[13:06:50.488][D][main:174]: Error: Response status: 401, message You do not have permission to view this resource, Please Login
[13:06:51.472][E][component:343]: http_request cleared Error flag