Skip to content
Text Post API

Module 01 · foundations · beginner

Lesson 01 · Text Post API

Goal

Build a tweet-style "create post" API in n8n: it accepts a text, checks it is 1–280 characters, and responds with the created post or an error. Almost every real automation starts with a trigger like this, whether it's a form submission, a Stripe event or a frontend calling your workflow.

Prerequisites

  • n8n running locally (playground/, docker compose up -d)
  • curl in your terminal

Concepts

  • Items: every node receives and emits a list of items shaped like [{ json: {...} }]. Click any node after a run to see its input and output.
  • Expressions: {{ ... }} is JavaScript evaluated per item. $json is the current item; the webhook payload lives under $json.body. Plain JavaScript works inside, so {{ $json.body.text.length }} counts characters and {{ $json.body.author || 'anonymous' }} sets a default.
  • Test vs production URL: /webhook-test/posts works once each time you click Listen for test event. /webhook/posts works only when the workflow is published.
  • Respond to Webhook: with the Webhook's response mode set to Using Respond to Webhook Node, you decide exactly what the caller gets back, including the status code.
  • Status codes: 201 Created means "I made the thing you asked for". 400 Bad Request means "your input is wrong, fix it and try again".

Build it

Step 1: Webhook trigger

Add a Webhook node. Set:

  • HTTP Method: POST
  • Path: posts
  • Respond: Using 'Respond to Webhook' Node

Step 2: Validate with IF

Add an IF node after the Webhook and rename it Valid Post?. Add two conditions and keep the combinator on AND:

  1. Value {{ $json.body.text }}, operator String → is not empty
  2. Value {{ ($json.body.text || '').length }}, operator Number → is less than or equal to, value 280

Use is not empty, not exists: {"text": ""} exists but is empty. The || '' in the second condition stops the expression failing when text is missing.

Step 3: Success branch

From the true output, add Edit Fields (Set) named Build Post with:

  • id (String) = {{ $execution.id }}
  • author (String) = {{ $json.body.author || 'anonymous' }}
  • text (String) = {{ $json.body.text }}
  • length (Number) = {{ $json.body.text.length }}
  • createdAt (String) = {{ $now.toISO() }}

Then a Respond to Webhook named Respond Created: Respond With JSON, body {{ $json }}, Options → Response Code = 201.

Step 4: Error branch

From the false output, add Edit Fields (Set) named Build Error with:

  • error (String) = text must be 1-280 characters (no quotes: n8n already knows it's a string)

Then a Respond to Webhook named Respond Error: Respond With JSON, body {{ $json }}, Options → Response Code = 400.

Step 5: Save and publish

Save (Cmd/Ctrl+S), then click Publish.

Test it

./test/test.sh

Expected:

PASS valid post (201) {"id":"...","author":"Ian","text":"Shipped my first n8n API","length":24,"createdAt":"..."}
PASS default author (201) {"id":"...","author":"anonymous","text":"First post from n8n","length":19,"createdAt":"..."}
PASS missing text (400) {"error":"text must be 1-280 characters"}
PASS empty text (400) {"error":"text must be 1-280 characters"}
PASS too long (400) {"error":"text must be 1-280 characters"}

Common problems

  • 404 "webhook not registered": test URL without clicking Listen, or production URL without publishing.
  • Every post is rejected: you forgot -H 'Content-Type: application/json', so the body isn't parsed as JSON.
  • Request hangs: a branch ends without a Respond to Webhook node.
  • Success returns 400: you duplicated the error Respond node for the success branch and kept its response code. Each Respond node has its own status code.
  • Empty posts get through: the first condition uses exists instead of is not empty.
  • IF errors on missing text: the length condition is {{ $json.body.text.length }} without || '', which fails when there is no text.
  • Error text has extra quotes: you typed "text must be 1-280 characters" with quotes into the String field.