Integrating Your Serverless Telegram Bot with AWS API Gateway

Will Kelly
7 min readDec 10, 2019


This is a continuation from my last article on Running a Serverless Telegram Bot from AWS Lambda. Before that, I also wrote a simple guide to set up the basics, Send and Receive Messages with the Telegram API.

If this is the first post you have come across, no worries. Here is a run down on our current situation with a AWS Lamba function:

  • We receive data as a JSON payload in the following format:
"name":"Test Name",
"message":"This is an example message"
  • Other key-value data included is ignored
  • We raise an exception after basic input validation
  • We return the response from the Telegram API call with either a 200 or 400 status code in JSON format:
"statusCode": 200,
"body": {},

Alright, let’s get started.

Navigate to API Gateway from the console. It is located under “Networking and Content Delivery.”

Select REST, New API, and pick your API name to suit your needs. You can see below mine is named “Contact-Medium”

Then, create a new API using the REST protocol config:

Next, click the actions drop-down and select create method. Typically I would create a resource (corresponding to a noun endpoint like “/message” ), but since we only have one endpoint we will just add a POST method on the root.

Click on the green POST after clicking the check-mark, and select Lambda function as the integration type.

Go down to Lambda function and type in the name of your lambda function. In this case mine was named “send-telegram-message.” The field should auto-fill.

Click save, and you should get a popup to allow API Gateway to invoke your Lambda function. Click ok to accept.

You should now see a diagram with 6 boxes. The far left box “Client” has a Test button with a lightning bolt under it. Click it and you should see this page:

Let’s add our valid JSON payload that we used in the Lambda test case from the last article, changing the message to “from API Gateway”:

"name": "Test Name",
"email": "",
"message": "Hello this is a test message from API Gateway"

Click the test button and you should receive a Telegram message! To the right side you should also be shown the Response Body, Response Headers, and Logs.

What happens if you try and empty Request Body?

Can you spot what is wrong here?

Well, for one, there is a lot of data here the client should probably not be exposed to — like the stack trace. It is also poorly formatted, shouldn’t the field errors be parsed into JSON?

Is there anything else?

It took me a while to notice, but we are actually getting a 200 status code for an error! This would definitely cause some confusion on the client side when processing the response.

Click back to Method Execution.

If you click on the tooltip (an orange circle with a blue dot inside it) of “Integration Response,” a relevant passage will appear. The second sentence describes what we are looking to fix — how Lambda errors and HTTP status codes are mapped to our API Gateway method.

Click on Integration Response.

You will see the following screen, with one default mapping for the 200 status code.

We need to add another response for our error case, so click the Add integration response button.

However, if you look at the second field, we need a “Method response status,” and since we have not created any, the drop-down is empty.

Navigate back to Method Execution, and click on Method Response.

Click on Add Response, and notice it is only asking for a status code. Our error is a result of not supplying the required parameters, so we can look up what is appropriate response here:

I am a bit conflicted, and there is a debate mostly between 400 and 422 here:

After reading the responses, I decided to stick with 400, but feel free to make your own decision.

Now that we have a Response status code, let’s head back to Integration Response.

Click on Add integration response again, and we will fill out the form.

Our first input in Lambda Error Regex. Regex matches over the response from Lambda, so it is fairly intuitive to search for “error” in that response. We will use the regex syntax “.*error.*” to check if error occurs anywhere, allowing for anything before or after. Make sure to type it without quotes in the form.

Then select our error code in the drop-down.

For Content handling, select Convert to text (if needed), so we can process our error message.

If you run another test, you will receive a 400 status code, but you will still have a messy Response Body.

Let’s fix that by adding a Mapping Template to the error Integration Response.

Click the error row in the table, then expand the Mapping Template section. Add a mapping template, using the Content-Type application/json. You can select “Error” from the Generate template drop-down, but we will customize this response a bit. The syntax may be a bit foreign, but you can copy my example. All we are doing is parsing the error message JSON and assigning it to a new JSON payload under the message key.

When you are finished, click Save and your Method Response should look like this:

Now if you return to Method Execution and test the empty body, you get the expected result with a clean, readable Response Body!

Now that our testing works as expected, let’s deploy our API.

Navigate back to Method Execution and click the Actions drop-down. Select Deploy API.

Select New Stage, and create a Stage name. I’m using the name Dev. Click Deploy, and you will be directed to the Stages tab. You’ll see a blue highlighted Invoke URL for your endpoint, with a bunch of different settings. Copy the URL and call the API with your message of choice.

Depending on where you are calling this API from (like the browser), you might have an issue. If all is well, you can skip this section that deals with CORS.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at {YOUR_API_GATEWAY_ENDPOINT} (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

If you are not already familiar with CORS, then there are plenty of articles across the web to help you out with the concept. As usual, the Mozilla Developer Network is a fantastic resource.

We will go ahead and skip to the practical fix — we need our endpoint provide the Access-Control-Allow-Origin header.

Navigate back to your API Gateway endpoint. Click the Resources tab — not staging — and select your POST endpoint. Click the action drop-down and select Enable CORS. I checked Default 4XX and Default 5XX, and left the Allow-Origin as ‘*’. If you want to restrict the domain, you could do that here.

Redeploy the API. If you change the stage name, ensure you update the endpoint name from wherever you are calling this API from.

You should receive a Telegram message, which is pretty exciting.

There is one catch though. If we try to submit the form with an empty field, we still get a Cross Origin Request Blocked warning. We already know we should be receiving an error for this submission, so our debugging intuition might point us back to the Method Response section of API Gateway.

If we head back there and examine what is working — the successful (200) response — we see that it has a CORS header.

But when we expand the 400, we see that it does not have any headers.

Add an Access-Control-Allow-Origin header to the error response.

Hm, does it seem like something is missing?

Well, we have a header key, but we did not set the value anywhere. Save the header name and navigate back to the Integration Response.

Open up the error/400 row, and there is a Header Mapping section. When we expand it, we see the Access-Control-Allow-Origin header, and we can set the value there.

Set the mapping value as ‘*’ (scare quotes required), save, and we should be good to go.

You can redeploy the API as you wish.

Suggested modifications / further improvements:

  • Adding a rate limit
  • Add some kind of authorization, perhaps with IAM or Cognito.
  • You might want to disable CORS.

Sign up for more tutorials & updates at

More sign ups ⇒ more content

Check out my personal site:

Thanks for reading.