How to integrate Alexa with Home Assistant the "raw" way (Skill + Lambda + IAM + Alexa Media Player)

How to integrate Alexa with Home Assistant the "raw" way (Skill + Lambda + IAM + Alexa Media Player)
Reading time: 12 min read
Link copied!

Integrating Alexa with Home Assistant using the “official and complete” path is the kind of thing that works really well… after you get punched a few times along the way.

Here I’m going to leave the full step-by-step, from zero. The boring parts (Skill, Lambda, IAM, correct region), the critical parts (HTTPS/443 and account linking), and the fun part (Alexa Media Player to make your house talk).

This is the Home Assistant “Smart Home Skill” path. It’s more work, but it’s what gives you the real smart home experience, like saying “Alexa, turn off the lights” without having to mention any skill name. The official HA docs describe this flow and its requirements, including the need for valid HTTPS/443 and a trusted certificate.


Prerequisites you need to have sorted out first

Your Home Assistant must be accessible from the internet over HTTPS, using a valid certificate (Let’s Encrypt is fine) and, ideally, on port 443. Self-signed certificates do not work for account linking.

That usually means one of these options (pick your poison):

I won’t go too deep here, because every setup is its own universe. Just keep this rule in mind: Alexa account linking gets very picky if you move away from “valid HTTPS on port 443”. The Home Assistant docs repeat this a lot.


Step 1. Create the Smart Home Skill in Alexa Developer Console
  1. Go to the Alexa Developer Console and create a new skill.
  2. Choose “Smart Home” and “Provision your own”.
  3. Make sure it’s using payload v3 and take note of the Skill ID. You’ll need it for Lambda.
Use the same Amazon account you use in the Alexa app and on your Echo devices. Otherwise you’ll create a beautiful skill that never shows up where it matters.
Step 2. Create the IAM Role (so Lambda can run)

In the AWS IAM Console, create a Role for the “Lambda” service. Attach the AWSLambdaBasicExecutionRole policy.

That’s it. Don’t overthink this part.

The Home Assistant docs describe exactly this path, including citing the policy.


Step 3. Create the Lambda function (and pick the right region)
Here’s a common trap: the Lambda region must be compatible with the skill locale, not where you physically live.

For en-US (and also pt-BR), Home Assistant recommends US East (N. Virginia). If you create the Lambda in another region, the setup may look finished but the skill simply won’t work, with no obvious error.

So:

  1. In AWS Lambda Console: Create function → Author from scratch
  2. Runtime: Python 3.x (use the latest available)
  3. Execution role: use the role you created earlier
  4. Add trigger: “Alexa Smart Home”
  5. Paste your Skill ID

Step 4. Lambda code (the proxy to /api/alexa/smart_home)

At the end of the day, this Lambda does one thing only: it receives what Alexa sends and forwards it straight to Home Assistant’s /api/alexa/smart_home endpoint. No real magic here.

The Home Assistant docs tell you to copy a Python script from a specific gist, already adapted for some scenarios like proactive events.

Below is a clean, formatted version of lambda_function.py, based on that gist and working well in practice:

"""
Alexa Smart Home Skill Adapter for Home Assistant
Based on the community gist referenced by Home Assistant documentation.

Env vars:
- BASE_URL (required): https://your-domain (no trailing slash)
- NOT_VERIFY_SSL (optional): True to ignore SSL issues (not recommended)
- DEBUG (optional): True for verbose logs and token fallback
- LONG_LIVED_ACCESS_TOKEN (optional): debug/testing only
"""
import json
import logging
import os
from typing import Any, Dict

import urllib3

_debug = bool(os.environ.get("DEBUG"))

_logger = logging.getLogger("HomeAssistant-SmartHome")
_logger.setLevel(logging.DEBUG if _debug else logging.INFO)

logging.getLogger("urllib3").setLevel(logging.INFO)


def _mk_error(error_type: str, message: str) -> Dict[str, Any]:
    return {
        "event": {
            "payload": {
                "type": error_type,
                "message": message,
            }
        }
    }


def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
    _logger.info("Processing Alexa request")

    base_url = os.environ.get("BASE_URL")
    if not base_url:
        _logger.error("BASE_URL environment variable not set")
        return _mk_error("INVALID_REQUEST", "BASE_URL environment variable must be set")

    base_url = base_url.rstrip("/")

    directive = event.get("directive")
    if not directive:
        return _mk_error("INVALID_REQUEST", "Request missing directive")

    payload_version = directive.get("header", {}).get("payloadVersion")
    if payload_version != "3":
        return _mk_error("INVALID_REQUEST", f"Only payloadVersion 3 is supported, got {payload_version}")

    scope = (
        directive.get("endpoint", {}).get("scope")
        or directive.get("payload", {}).get("grantee")
        or directive.get("payload", {}).get("scope")
    )

    if not scope or scope.get("type") != "BearerToken":
        return _mk_error("INVALID_REQUEST", "Request missing BearerToken scope")

    token = scope.get("token")

    if not token and _debug:
        token = os.environ.get("LONG_LIVED_ACCESS_TOKEN")

    if not token:
        return _mk_error("INVALID_AUTHORIZATION_CREDENTIAL", "Authentication token is required")

    verify_ssl = not bool(os.environ.get("NOT_VERIFY_SSL"))
    http = urllib3.PoolManager(
        cert_reqs="CERT_REQUIRED" if verify_ssl else "CERT_NONE",
        timeout=urllib3.Timeout(connect=2.0, read=10.0),
    )

    try:
        resp = http.request(
            "POST",
            f"{base_url}/api/alexa/smart_home",
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
            },
            body=json.dumps(event).encode("utf-8"),
        )

        if resp.status >= 400:
            body = resp.data.decode("utf-8")
            _logger.error("Home Assistant returned %s: %s", resp.status, body)

            error_type = "INVALID_AUTHORIZATION_CREDENTIAL" if resp.status in (401, 403) else "INTERNAL_ERROR"
            return _mk_error(error_type, body)

        return json.loads(resp.data.decode("utf-8"))

    except Exception as e:
        _logger.exception("Unexpected error: %s", str(e))
        return _mk_error("INTERNAL_ERROR", "An unexpected error occurred")

After pasting the code, click Deploy.

Step 5. Lambda environment variables (don’t skip this)

In the Lambda configuration, go to Configuration -> Environment variables and add:

  • BASE_URL = your public Home Assistant URL (no trailing slash)
    Example: https://home.yourdomain.com

Optional, if needed:

  • DEBUG = True (for testing and logs)
  • LONG_LIVED_ACCESS_TOKEN = debug/testing only, remove later
  • NOT_VERIFY_SSL = True (only for temporary testing if you really know what you’re doing)

The official Home Assistant docs describe these variables and also warn about the risk of leaving tokens in there.

Step 6. Configure the Smart Home Skill endpoint (Lambda ARN)

Back in Alexa Developer Console: Build -> Smart Home -> Endpoint (Smart Home service endpoint)

Paste your Lambda ARN as the Default endpoint.

The HA docs describe this exact step.

Step 7. Account Linking (where most people get stuck)

In Alexa Developer Console: Build -> Account Linking

Set it up like this:

  • Authorization URI: https://YOUR_HA/auth/authorize
  • Access Token URI: https://YOUR_HA/auth/token

If your HA is not reachable over clean HTTPS (preferably 443), this becomes a frustration factory.

The HA docs also list the Client IDs for “Login with Amazon” (pitangui/layla) and mention that Let’s Encrypt is accepted.

Important details:

  • Do NOT enable “linking from within your application”, because that forces a redirect URI that doesn’t work with HA’s flow
  • Authentication Scheme: “Credentials in request body”
  • Scope: create “smart_home”
Step 8. Home Assistant configuration (configuration.yaml)

Inside Home Assistant, add at least:

alexa:
  smart_home:
    locale: en-US

If you stop here, Alexa will try to expose everything, which usually turns into a mess.

I strongly recommend filtering:

alexa:
  smart_home:
    locale: en-US
    filter:
      include_domains:
        - light
        - switch
        - cover
        - climate
      include_entity_globs:
        - binary_sensor.*_motion
      exclude_entities:
        - switch.something_you_dont_want

The HA docs have filter examples and explain the order of application of includes/excludes.

Restart Home Assistant.

Step 9. Enable the skill and discover devices

In the Alexa app (or mobile app): More -> Skills & Games -> Your Skills -> Dev

Enable your skill.

It will open Home Assistant login (account linking). If everything worked, discovery starts right away.

The HA docs describe this flow and “Discover devices” as well.

Testing the Lambda first (optional, but I like it)

The HA docs suggest creating a Discovery test event in the Lambda console. If you enabled DEBUG and added LONG_LIVED_ACCESS_TOKEN, this test is easy.

Example test payload (Discovery) that the HA docs use:

{
  "directive": {
    "header": {
      "namespace": "Alexa.Discovery",
      "name": "Discover",
      "payloadVersion": "3",
      "messageId": "1bd5d003-31b9-476f-ad03-71d471922820"
    },
    "payload": {
      "scope": {
        "type": "BearerToken"
      }
    }
  }
}

It explains that without a token, Lambda will only work if you’re in DEBUG mode with LONG_LIVED_ACCESS_TOKEN configured.


Exposing scripts and making Alexa run them

Scripts are supported by the Alexa Smart Home integration, but they don’t always show up nicely.

In practice, what works best:

  • expose the script
  • put it as an action in a Routine (when the app shows it)
  • or create an input_boolean/switch and use automation to trigger the script (this almost always shows up well in Alexa)

I use the input_boolean approach a lot. It’s predictable.

Create a script in Home Assistant (real example)

Example script that speaks a basic house status. I’ll keep it simple, but you can see the idea.

Put it in scripts.yaml (or create via UI and adjust if you want):

house_status:
  alias: "House Status"
  sequence:
    - variables:
        lights_on: "{{ states.light | selectattr('state','eq','on') | list | length }}"
    - service: notify.alexa_media_echo_living_room
      data:
        message: >
          {% if lights_on == 0 %}
            All lights are off.
          {% else %}
            There are {{ lights_on }} lights on.
          {% endif %}
        data:
          type: announce

Important notes:

  • I’m using notify.alexa_media_echo_living_room, which comes from Alexa Media Player (we’ll install it soon)
  • I’m using announce to play as an announcement
  • If you tried using tts.google_translate_say and got an error, that’s because you don’t have that service/config (and honestly, with Alexa Media Player you don’t need it)
Make sure the script appears in Alexa (via filter)

In configuration.yaml, include this script explicitly so it doesn’t depend on “include_domains”:

alexa:
  smart_home:
    locale: en-US
    filter:
      include_entities:
        - script.house_status

Restart HA, and tell Alexa to “Discover devices”.

Plan B that always works: create a “button” (input_boolean) and an automation

Create a helper input_boolean:

input_boolean:
  trigger_house_status:
    name: "Trigger House Status"
    icon: mdi:home-analytics

Expose this input_boolean to Alexa:

alexa:
  smart_home:
    locale: en-US
    filter:
      include_entities:
        - input_boolean.trigger_house_status

And create an automation that, when turned on, triggers the script and turns the boolean back off:

automation:
  - alias: "Alexa - Run House Status"
    trigger:
      - platform: state
        entity_id: input_boolean.trigger_house_status
        to: "on"
    action:
      - service: script.house_status
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.trigger_house_status

In Alexa, this appears as a switch. Then you can create Routines, voice commands, whatever you want.

Is it ugly? Yes. But it’s the ugly that works.

Step 10. Installing Alexa Media Player (TTS, announcements, Echo control)

Alexa Smart Home Skill is for controlling HA entities.
Alexa Media Player is for controlling Alexa devices from Home Assistant and making them speak.

It’s a custom component (usually via HACS) and uses an unofficial API, so yes, it can break if Amazon changes something. The repository itself makes this warning clear.

The recommended installation is via HACS.

Install via HACS (Easy Mode)
  1. Go to Settings -> Devices & Services -> Add Integration -> Alexa Media Player
  2. Log in and configure 2FA with Authenticator App Key (yes, this is important).

The project wiki explains the 2FA flow and how to get the “Enter your key” (the secret) on Amazon’s approval page.

After configuring, you’ll get:

  • entities media_player.echo_*
  • services notify.alexa_media_*
  • speak/announce actions (depending on version)
Ready-to-use examples I use a lot with Alexa Media Player

Announce on a specific Echo

service: notify.alexa_media_echo_living_room
data:
  message: "Front door opened."
  data:
    type: announce

Speak as normal TTS (not announce)

service: notify.alexa_media_echo_living_room
data:
  message: "Good night. Lights off."
  data:
    type: tts

Announcement on multiple Echos (group)

You can call more than one notify.* in sequence, or create a “broadcast” script:

script:
  announce_to_all:
    alias: "Announce to all Echos"
    fields:
      msg:
        description: "Message"
        example: "Test"
    sequence:
      - service: notify.alexa_media_echo_living_room
        data:
          message: "{{ msg }}"
          data:
            type: announce
      - service: notify.alexa_media_echo_bedroom
        data:
          message: "{{ msg }}"
          data:
            type: announce

Troubleshooting: when “nothing appears” in Alexa

If you tell Alexa to “Discover devices” and it says it found nothing, it’s usually one of these:

  • HA is not accessible externally with valid HTTPS (account linking fails or works “halfway”)
  • Lambda in the wrong region for the skill locale (en-US usually is N. Virginia)
  • BASE_URL in Lambda is wrong (with trailing slash, or pointing to internal URL)
  • you forgot to restart HA after editing configuration.yaml
  • filter is excluding everything by accident (start by exposing 1 entity and grow)
  • you enabled the skill in another Amazon account that’s not the one on your Echos (classic)

After the initial pain, this setup is rock solid. Native voice control through Smart Home Skill for “doing things”, and Alexa Media Player for making the house talk.

Once it’s done, you don’t really want to go back.