EchtPost API v2 — AI consumer guide
This guide is written for LLM-based clients and developers wiring up integrations. Endpoints, error shapes, and examples are kept as copy-paste-ready as possible.
Base URL
https://api.echtpost.de/v2
Authentication
The API accepts two credential types — both sent in a header. Query-string
parameters such as ?apikey= are not accepted on v2.
Recommended: OAuth 2.1 (user-delegated access)
Use OAuth when the integration acts on behalf of a real EchtPost user (Claude, ChatGPT, third-party apps with a UI). Tokens are short-lived, scoped to the user's chosen account, and can be revoked from the user's side without touching API keys.
Discovery URLs:
https://api.echtpost.de/.well-known/oauth-protected-resource # RFC 9728
https://my.echtpost.de/.well-known/oauth-authorization-server # RFC 8414
A 401 response from any v2 endpoint sets WWW-Authenticate: Bearer
resource_metadata="https://api.echtpost.de/.well-known/oauth-protected-resource",
which points at the protected-resource document. That document declares
https://my.echtpost.de as the authorization server; clients then walk
the RFC 8414 metadata there to find the authorize / token / register
endpoints. Tokens must include the api scope to be accepted on the
REST API. A token scoped only for mcp will be rejected here (and
vice versa on the MCP server). Request scope=mcp api if you need both.
The flow is OAuth 2.1 Authorization Code with PKCE. Once you have an access token:
curl -sS -H "Authorization: Bearer $ECHTPOST_OAUTH_TOKEN" \
"https://api.echtpost.de/v2/me"
Dynamic client registration (RFC 7591) is supported at
POST https://my.echtpost.de/oauth/register if you don't want to manually
register a client.
Alternative: API key (server-to-server)
Use an API key for server-side scripts and automations where there is no end-user to consent. Create keys in the EchtPost web app under Account → API keys.
Supported headers (in order of precedence):
X-Api-Key: <your key>Authorization: Bearer <your key>
V1's X-apikey header is not accepted on v2.
curl -sS -H "X-Api-Key: $ECHTPOST_API_KEY" \
"https://api.echtpost.de/v2/me"
Rate limits
Requests are throttled per API key. Exceeding a limit returns 429 Too Many Requests
with a Retry-After header (seconds until the window resets).
| Scope | Limit |
|---|---|
| All endpoints | 100 requests / minute |
POST /v2/cards |
10 requests / minute |
POST /v2/cards/from_template |
10 requests / minute |
POST /v2/cards/preview_fit |
30 requests / minute |
Spread bulk card creation over time or batch recipients into a single card request
using recipient_ids to stay within limits.
Error format (RFC 7807)
Errors use Content-Type: application/problem+json. Each response
includes:
type— stable error-type URI (match on this, not on free text)title— short human-readable namestatus— HTTP status codedetail— imperative fix hint (tells the integrator how to retry)instance— URL path that produced the errorrequest_id— for support ticketserrors[]— optional structured field errors (path,code,message)
Example:
{
"type": "https://api.echtpost.de/v2/errors/text-does-not-fit",
"title": "Message does not fit on the card",
"status": 422,
"detail": "Shorten the message or reduce the font size; 1 line overflows.",
"instance": "/v2/cards",
"request_id": "req-abc123",
"errors": [
{ "path": "message", "code": "overflow", "message": "line 17 overflows" }
]
}
Retry guidance
detail is phrased imperatively — it tells you exactly what to change
before retrying. Treat 422 responses as recoverable after applying
the fix described in detail. 401 is terminal (bad key). 402 means
top-up required. 409 on card cancel means the state transition is no
longer possible.
Pagination
List endpoints return a JSON array. Read pagination from response headers:
X-Total-Count— total rowsX-Total-Pages— total pages for currentper_pageLink— RFC 5988 links withrel="next",rel="last"
Request parameters:
page(default 1)per_page(default 50, max 200)
List filters (query parameters)
Use top-level query parameters only. v2 does not support nested
filter[...].
| Endpoint | Parameters |
|---|---|
GET /v2/contacts |
country_code, group_id, external_id, search, plus sorting |
GET /v2/cards |
status (pending, scheduled, sent, canceled; comma-separated allowed), deliver_at, motive_id, plus sorting |
GET /v2/motives |
search (name, description, content, searchterms), plus sorting |
GET /v2/groups |
sorting only |
GET /v2/templates |
source (defaults to regular) |
List sorting
List endpoints accept optional sort (one allowed field name) and
direction (asc or desc, case-insensitive). Omitted values use
the defaults below. Results always include a stable tie-breaker on the
row id ascending.
Unknown sort or invalid direction (when direction is sent) returns
422 with RFC 7807 application/problem+json.
| Endpoint | Default sort |
Default direction |
Allowed sort values |
|---|---|---|---|
GET /v2/contacts |
created_at |
desc |
last_name, created_at, updated_at |
GET /v2/cards |
created_at |
desc |
created_at, deliver_at |
GET /v2/motives |
priority¹ | — | name, created_at |
GET /v2/groups |
name |
asc |
name, created_at, updated_at |
¹ When no sort parameter is given, motives are ordered by relevance:
account-specific motives first, then highlighted, then by popularity.
Card status in JSON
Card responses expose four statuses: pending (draft through
uploaded), scheduled, sent, canceled (includes canceling).
Internal workflow states are not exposed in the API.
Fonts
Three families, each with its own valid size range:
| family | valid sizes |
|---|---|
architects_daughter |
12..14 |
reenie_beanie |
15..17 |
special_elite |
11..13 |
Colors: black, blue, or a hex value.
Line breaks in card content
Card content is plain text. Use \n (Unix newline) as the line-break character
in your JSON strings — the API normalizes it to the internal \r\n format used
by the print renderer. Sending \r\n directly is also accepted.
A card with no line breaks at all will render as one unbroken text block.
The create endpoint returns a warnings array in the 201 response with a
no_line_breaks entry if this is detected — treat it as a hint to add
paragraph breaks before submitting.
Quick flow — one card in three calls
GET /v2/me— confirm balance and account metadata.POST /v2/cards/preview_fit— iteratecontent+fontuntilfits: true.POST /v2/cards— create the mailing withrecipient_idsor inlinerecipients.
Step 1 — check balance
curl -sS -H "X-Api-Key: $ECHTPOST_API_KEY" \
"https://api.echtpost.de/v2/me"
Step 2 — check that the text fits
curl -sS -H "X-Api-Key: $ECHTPOST_API_KEY" \
-H "Content-Type: application/json" \
-X POST "https://api.echtpost.de/v2/cards/preview_fit" \
-d '{
"message": "Liebe Grüße aus Berlin,\ndas Team",
"font": { "family": "architects_daughter", "size": 13 }
}'
Response on success:
{ "fits": true, "lines_used": 2, "max_lines": 16 }
On overflow the response includes overflow_line and a
suggested_font_size that would make the text fit:
{
"fits": false,
"lines_used": 18,
"max_lines": 16,
"overflow_line": 17,
"suggested_font_size": 11
}
Step 3 — create the card
curl -sS -H "X-Api-Key: $ECHTPOST_API_KEY" \
-H "Content-Type: application/json" \
-X POST "https://api.echtpost.de/v2/cards" \
-d '{
"motive_id": 42,
"content": "{anrede},\nwir wünschen frohe Ostern.\nHerzlich, das Team",
"font": { "family": "architects_daughter", "size": 13 },
"recipient_ids": [101, 102]
}'
The {anrede} token is substituted per recipient at render time with the
recipient's salutation (e.g. "Sehr geehrte Frau Müller" or "Liebe Jana").
Use it anywhere in content, content_ps, or content_vertical.
The canonical form is {anrede} (lowercase); casing is ignored at render time.
Endpoint quick reference
GET /v2/me
Account, user, API key, balance.
curl -sS -H "X-Api-Key: $KEY" "https://api.echtpost.de/v2/me"
GET /v2/credits
Balance plus current prices:
curl -sS -H "X-Api-Key: $KEY" "https://api.echtpost.de/v2/credits"
Contacts
# list with filter + sort
curl -sS -H "X-Api-Key: $KEY" \
"https://api.echtpost.de/v2/contacts?country_code=de&sort=last_name&direction=asc"
# create
curl -sS -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-X POST "https://api.echtpost.de/v2/contacts" \
-d '{
"last_name":"Doe","first_name":"Jane",
"street":"Musterstr. 1","zip":"10115","city":"Berlin","country_code":"de",
"greeting_style":"formal"
}'
# update
curl -sS -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-X PATCH "https://api.echtpost.de/v2/contacts/42" \
-d '{ "city":"München" }'
# delete
curl -sS -H "X-Api-Key: $KEY" -X DELETE "https://api.echtpost.de/v2/contacts/42"
Groups
# list
curl -sS -H "X-Api-Key: $KEY" "https://api.echtpost.de/v2/groups"
# create
curl -sS -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-X POST "https://api.echtpost.de/v2/groups" -d '{ "name": "VIPs" }'
Deleting a group that still has members returns 409 Conflict.
Templates
curl -sS -H "X-Api-Key: $KEY" "https://api.echtpost.de/v2/templates"
curl -sS -H "X-Api-Key: $KEY" "https://api.echtpost.de/v2/templates/3"
Motives
Browse available postcard designs. Use the returned id as motive_id
when creating cards.
# list all motives
curl -sS -H "X-Api-Key: $KEY" "https://api.echtpost.de/v2/motives"
# search by keyword
curl -sS -H "X-Api-Key: $KEY" \
"https://api.echtpost.de/v2/motives?search=geburtstag"
# show a specific motive
curl -sS -H "X-Api-Key: $KEY" "https://api.echtpost.de/v2/motives/42"
Each motive includes feature flags (content_available,
content_ps_available, vertical_text_available, qr_code_available)
that indicate which content fields are supported, plus a url linking
to the motive's page on echtpost.de.
Cards
deliver_at field
The deliver_at field accepts several formats:
| Value | Resolves to |
|---|---|
"YYYY-MM-DD" |
Exact date |
"today" |
Today |
"asap" |
Today (alias) |
"tomorrow" |
Tomorrow |
"N-days-from-now" |
N calendar days from now, e.g. "5-days-from-now" |
Constraints: must be a weekday; must not be a printer holiday. If omitted, the card is scheduled for the next available business day.
Natural-language strings like "in two days" are not accepted — use
the numeric hyphenated form "2-days-from-now" instead.
# list only scheduled + sent
curl -sS -H "X-Api-Key: $KEY" \
"https://api.echtpost.de/v2/cards?status=scheduled,sent&sort=deliver_at"
# show
curl -sS -H "X-Api-Key: $KEY" \
"https://api.echtpost.de/v2/cards/555"
# create from template
curl -sS -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-X POST "https://api.echtpost.de/v2/cards/from_template" \
-d '{
"template_id": 3,
"recipient_ids": [101, 102],
"deliver_at": "2026-05-12"
}'
# cancel (async)
curl -sS -H "X-Api-Key: $KEY" -X DELETE "https://api.echtpost.de/v2/cards/555"
Cancel returns 202 Accepted when the cancellation is queued, or
409 Conflict with type: card-not-cancelable if the state transition
is no longer possible (e.g. the card is already sent).
Notifications (optional on create)
"notification": { "type": "on_send", "email": "ops@acme.de", "send_on": "send" }
Stable error types
Match integrations on these type URIs:
| URI suffix | HTTP | When |
|---|---|---|
authentication-required |
401 | Missing or invalid API key |
not-found |
404 | Resource not found or not in scope |
validation |
422 | Generic validation — see errors[] |
text-does-not-fit |
422 | Message overflows card at given font |
invalid-delivery-date |
422 | deliver_at is not a valid business date — use YYYY-MM-DD, today, tomorrow, or N-days-from-now and pick a weekday that is not a public holiday |
template-not-available |
422 | Template not usable by this account |
feature-not-available |
422 | E.g. QR-code cards not enabled |
insufficient-funds |
402 | Balance too low for the requested cards |
card-not-cancelable |
409 | Card is past the cancelable window |
rate-limited |
429 | Too many requests — check Retry-After header |
All full URIs are prefixed with https://api.echtpost.de/v2/errors/.