Links
Einen Deep Link je Produkt, Artikel oder Einladung anlegen, mit seiner URL lesen und wieder löschen. Wie viele Links ein Projekt haben darf, hängt vom Tarif ab, in Free 5.000.
REST · JSON · Server-Keys
Dein Backend legt Links an, sendet Kampagnen und Pushes an einzelne Nutzer, meldet Events und löscht Nutzer. REST mit JSON über HTTPS, mit einem Server-Key, der nur kann, was seine Scopes erlauben.
Als NächstesGebaut und getestet. Die Abnahme auf der Produktionsinstanz steht noch aus.
Einen Deep Link je Produkt, Artikel oder Einladung anlegen, mit seiner URL lesen und wieder löschen. Wie viele Links ein Projekt haben darf, hängt vom Tarif ab, in Free 5.000.
Eine Kampagne für ein gespeichertes Segment anlegen, sofort senden oder planen und ihren Report lesen. Dieselben Kampagnen, Quoten und Reports wie in der Console.
Ein Push an einen Nutzer oder eine Installation, etwa „Deine Bestellung ist unterwegs“, mit dem Screen deiner App, den er öffnet.
Melden, was dein Backend zuerst weiß, etwa einen Kauf oder eine Verlängerung. Funnels, Segmente und Journeys zählen es wie ein Event aus der App.
Wenn jemand sein Konto bei dir schließt, löschst du ihn auch in Lotse, mit seinen Installationen und allem, was sie geschickt haben.
Ein Server-Key gehört zu einem System, nicht zu einer Person. Er funktioniert weiter, wenn die Kollegin, die ihn angelegt hat, das Projekt verlässt. Nur der Besitzer eines Projekts legt Keys an, rollt und widerruft sie; Mitglieder sehen sie.
In der Console dein Projekt öffnen, dann Developers und Server keys, und Create key wählen. Gib ihm einen Namen, den du wiedererkennst, etwa „Shop-Backend“. Ein Projekt hat bis zu zehn aktive Keys.
Mit einer Vorlage beginnen (Links, Campaigns, Transactional pushes, Server events, User deletion) oder die Scopes selbst wählen. Sie stehen fest, sobald der Key existiert. Der Key läuft nach 90 oder 365 Tagen ab oder nie. Die Console zeigt, wann jeder Key zuletzt benutzt wurde.
Der Key beginnt mit lt_srv_ und wird genau einmal angezeigt. Lotse behält nur einen Hash davon. Er gehört auf deinen Server, nie in eine App oder eine Webseite.
Roll erzeugt einen neuen Key mit denselben Scopes. Der alte gilt noch 24 Stunden, während du den neuen ausrollst, oder endet sofort, falls er geleakt sein könnte. Revoke beendet einen Key endgültig.
links:readlinks:writecampaigns:readcampaigns:writecampaigns:sendmessages:sendevents:writeusers:deleteJede Route liegt unter https://api.lotse.dev/v1/. Allein der Key bestimmt das Projekt: Kein Pfad, keine Query und kein Body nennen eines.
Den Key im Header Authorization: Bearer lt_srv_… schicken. Ohne gültigen Key lautet die Antwort 401; der Public Key deiner App und Agent-Tokens sind keiner.
Bodies sind JSON-Objekte in UTF-8. Zeiten sind ISO 8601 mit Zeitzone, etwa 2026-10-20T09:00:00Z, und kommen in UTC zurück. Ein Feld, das die Route nicht kennt, wird abgelehnt, damit ein Tippfehler auffällt.
Browser schicken einen Origin-Header, und eine Anfrage damit bekommt 403, egal mit welchem Key.
Listen kommen neueste zuerst als {"data": [...], "nextCursor": "..."}. limit setzt die Seitengröße, ohne Angabe 100, und der nextCursor einer Seite, als cursor übergeben, liefert die nächste. null heißt, es war die letzte.
curl "https://api.lotse.dev/v1/links?limit=20" \
-H "Authorization: Bearer $LOTSE_SERVER_KEY"
Jede Route braucht einen Scope. Jedes Feld und jede Antwort steht in der OpenAPI-3.1-Beschreibung der API.
GET /v1/linkslinks:readLinks, neueste zuerst.
GET /v1/links/{id}links:readEin Link mit seiner URL.
POST /v1/linkslinks:writeLegt einen Link an: slug und targetPath, optional campaign, source, medium und ein Fallback je Plattform.
DELETE /v1/links/{id}links:writeLöscht einen Link.
GET /v1/segmentscampaigns:readDie gespeicherten Segmente des Projekts, um eine Kampagne zu adressieren.
GET /v1/campaignscampaigns:readKampagnen, neueste zuerst, ohne die Varianten von Journeys.
GET /v1/campaigns/{id}campaigns:readEine Kampagne mit Report. ?goalEvent= ergänzt, wer dieses Event binnen 24 Stunden erreicht hat.
POST /v1/campaignscampaigns:writeLegt einen Entwurf an: title, body, optional linkId, und die Zielgruppe als segmentId, segment oder "audience": "all".
POST /v1/campaigns/{id}/sendcampaigns:sendSendet einen Entwurf sofort, innerhalb der 20 Sendungen je 24 Stunden, die sich API und Console teilen.
POST /v1/campaigns/{id}/schedulecampaigns:sendPlant den Versand für sendAt, mindestens eine Minute und höchstens 30 Tage voraus.
POST /v1/messagesmessages:sendEin Push an eine userId oder installId: title, body, optional targetPath.
GET /v1/messages/{id}messages:sendEine Message mit dem Stand ihrer Zustellungen, und ob sie geöffnet wurde.
POST /v1/eventsevents:write1 bis 100 Events deiner Nutzer, über die Nutzer-ID.
DELETE /v1/users/{userId}users:deleteLöscht einen Nutzer mit seinen Installationen und Daten.
GET /v1/openapi.jsonohne KeyDie OpenAPI-3.1-Beschreibung aller Routen. Öffentlich, auch für einen Browser.
Mit curl, der Key in der Variablen LOTSE_SERVER_KEY. Jedes POST darf einen Idempotency-Key tragen, damit ein Retry nach einem Timeout nichts doppelt tut.
Ein Link je Produkt, angelegt, wenn das Produkt online geht. Die Antwort enthält die volle URL auf deiner Link-Domain.
curl https://api.lotse.dev/v1/links \
-H "Authorization: Bearer $LOTSE_SERVER_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: product-1042" \
-d '{
"slug": "rain-jacket",
"targetPath": "/products/1042",
"campaign": "catalogue",
"source": "shop",
"fallbackWeb": "https://shop.example.com/products/1042"
}'
{
"id": 318,
"slug": "rain-jacket",
"url": "https://harbor.lotse.dev/l/rain-jacket",
"targetPath": "/products/1042",
"campaign": "catalogue",
"source": "shop",
"fallbackWeb": "https://shop.example.com/products/1042",
"createdAt": "2026-10-21T09:14:03.512Z"
}
Das gespeicherte Segment nachschlagen, einen Entwurf dafür anlegen und senden. Ohne segmentId, segment oder "audience": "all" wird eine Kampagne abgelehnt, damit niemand aus Versehen an alle Geräte sendet.
# The saved segments of the project, with their ids
curl https://api.lotse.dev/v1/segments \
-H "Authorization: Bearer $LOTSE_SERVER_KEY"
# A draft for segment 7
curl https://api.lotse.dev/v1/campaigns \
-H "Authorization: Bearer $LOTSE_SERVER_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: spring-sale-draft" \
-d '{
"title": "Spring sale ends tonight",
"body": "Your basket is still waiting.",
"linkId": 318,
"segmentId": 7
}'
# Send it now, or plan it with POST .../schedule and {"sendAt": "..."}
curl -X POST https://api.lotse.dev/v1/campaigns/57/send \
-H "Authorization: Bearer $LOTSE_SERVER_KEY" \
-H "Idempotency-Key: spring-sale-send"
{
"id": 57,
"title": "Spring sale ends tonight",
"body": "Your basket is still waiting.",
"status": "sending",
"linkId": 318,
"segment": {
"all": [{ "field": "platform", "op": "eq", "value": "android" }]
},
"createdAt": "2026-10-21T09:14:03.512Z",
"scheduledAt": "2026-10-21T09:15:40.077Z"
}
Erreicht die Installationen von u_42, die sich mit gültigem User-Hash identifiziert haben. Angetippt öffnet der Push /orders/1042.
curl https://api.lotse.dev/v1/messages \
-H "Authorization: Bearer $LOTSE_SERVER_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-shipped" \
-d '{
"userId": "u_42",
"title": "Your order has shipped",
"body": "Order 1042 is on its way.",
"targetPath": "/orders/1042"
}'
{
"id": 905,
"status": "queued",
"userId": "u_42",
"title": "Your order has shipped",
"body": "Order 1042 is on its way.",
"targetPath": "/orders/1042",
"createdAt": "2026-10-21T09:14:03.512Z",
"deliveries": {
"pending": 2, "sending": 0, "sent": 0, "failed": 0, "invalidToken": 0
},
"opened": 0
}
Ein Kauf, den dein Backend bestätigt hat. Mit id speichert ein zweiter Aufruf mit demselben Event nichts und zählt es unter duplicates.
curl https://api.lotse.dev/v1/events \
-H "Authorization: Bearer $LOTSE_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"name": "purchase",
"userId": "u_42",
"id": "order-1042",
"ts": "2026-10-21T09:12:00Z",
"props": { "amount": "89.90", "currency": "EUR" }
}
]
}'
{ "accepted": 1, "duplicates": 0 }
Die Antwort ist 204 ohne Body, oder 404 für einen Nutzer, den das Projekt nicht kennt.
# The user id as the app passes it to identify, URL-encoded
curl -X DELETE https://api.lotse.dev/v1/users/u_42 \
-H "Authorization: Bearer $LOTSE_SERVER_KEY"
HTTP/1.1 204 No Content
Eine Message geht an genau ein Ziel: userId, die ID, die deine App an Lotse.identify übergibt, oder installId, die ID einer Installation.
Das Identitäts-Secret (lt_is_…) erzeugst du in der Console unter Settings, Identity verification. Dein Backend berechnet den Hash für den angemeldeten Nutzer und gibt ihn der App, die ihn an identify übergibt. Das Secret kommt nie in die App.
import crypto from 'node:crypto';
// HMAC-SHA256 of the user id, trimmed as Lotse stores it, keyed with the
// whole identity secret (lt_is_…), as lower-case hex.
const userHash = crypto
.createHmac('sha256', process.env.LOTSE_IDENTITY_SECRET)
.update(userId.trim())
.digest('hex');
await Lotse.instance.identify(userId, userHash: userHash);
POST /v1/events nimmt 1 bis 100 Events je Aufruf, jedes mit name und userId, optional mit ts, id und props. Ein fehlerhaftes Event lehnt den ganzen Aufruf ab; die Meldung nennt seinen Index.
Namen, die Lotse und sein SDK selbst schreiben, sind reserviert:
session_startidentifypush_sentpush_openlink_clickattribution_matchsurvey_answeredfeedback_submittedfeature_votedfeature_unvotedrage_tapDELETE /v1/users/{userId} tut dasselbe wie der Löschen-Knopf in der Console, in einer Transaktion. Auch ein gesperrtes Projekt kann löschen.
Eine Installation, die seitdem als jemand anderes identifiziert ist, bleibt mit allem, was sie geschickt hat. Ideen auf dem Feature-Board bleiben, ohne diese Stimmen. Der Webhook user.deleted sagt es deinen anderen Systemen, mit Nutzer-ID und Zeit und sonst nichts.
Schick zu jedem POST den Header Idempotency-Key mit: 1 bis 255 druckbare ASCII-Zeichen, einer je Arbeitsschritt, etwa order-1042-shipped. Lotse merkt ihn sich 24 Stunden je Server-Key.
Derselbe Key mit derselben Anfrage liefert die gespeicherte Antwort mit dem Header Idempotent-Replayed: true, und nichts passiert doppelt.
Derselbe Key mit anderem Pfad oder Body ist ein Konflikt. Läuft die erste Anfrage noch, kommt die 409 mit Retry-After.
Ein Retry mit demselben Key läuft erneut; nach einer 503 erledigt er, was fehlt, und nichts doppelt. Nach einer 500 oder einem Timeout läuft nichts ein zweites Mal: Der Retry bekommt die gespeicherte Antwort oder 409, mit Retry-After, solange die erste Anfrage noch laufen kann, danach mit dem Hinweis, dass ihr Ausgang unbekannt ist. Dann die Ressource per GET prüfen und mit neuem Key senden.
Jede Antwort außerhalb von 2xx ist JSON mit einem Code und einem englischen Satz für dich, nie mit einem Stacktrace.
{
"error": "forbidden",
"message": "This server key lacks the scope messages:send. Scopes are fixed when a key is created: create a key with it in the Lotse console."
}
invalid_inputunauthorizedforbiddenproject_suspendednot_foundmethod_not_allowedconflictinvalid_inputidentity_verification_requiredquota_exceededrate_limitedinternal_errorunavailableJeder Aufruf mit gültigem Key ohne 2xx bleibt 7 Tage in der Console, unter Developers, Request log: Zeit, Key, Methode, Pfad, Status, Code und Dauer, nie Body oder Header. Ablehnungen durch ein Budget nur stichprobenartig. Eine Nutzer-ID im Pfad wird nicht gespeichert.
Großzügig für ein Backend, eng genug, dass eines in einer Schleife gebremst wird. Über einer Grenze lautet die Antwort 429.
Je Key. Abgelehnte und gescheiterte Aufrufe zählen mit.
Je Key, innerhalb der 600: jedes POST und DELETE, das seine Route erreicht. Ein Retry, den sein Idempotency-Key beantwortet, kostet keinen.
Je Projekt, geteilt von allen seinen Keys.
Und 256 KiB für /v1/events, Platz für 100 typische Events.
Links je Tarif, in Free 5.000. 20 Kampagnen-Sendungen in 24 Stunden, gemeinsam mit der Console. 10 Messages je Nutzer und Stunde, 10.000 je Projekt und Tag. 100.000 Nutzer ohne Installation.
Solange ein Projekt gesperrt ist, lesen und löschen seine Keys weiter. Alles, was anlegt oder sendet, bekommt 403 project_suspended.
Kostenlos während der Beta. Registrieren, Projekt anlegen, SDK einbinden.