Die API von AM CRM

Zugriff auf Kontakte, Deals, Termine, Anrufe und Stammdaten einer Organisation — lesen und schreiben, mit Rechten, die du selbst festlegst. Dazu Ereignisse, die dein System benachrichtigen, statt dass es fragen muss.

Schlüssel anlegen: Einstellungen → API-Keys. Dort hakst du an, was er darf.

curl https://am-crm.com/api/v1/me \
  -H "Authorization: Bearer amk_…"

Antwortet das, stimmt alles: Schlüssel gültig, Organisation erkannt, Rechte sichtbar.

Anmeldung

Authorization: Bearer amk_…

Nur als Kopfzeile. Im Adressfeld (?api_key=) nimmt die v1-API keinen Schlüssel an: Adressen landen in Server-Protokollen, Browser-Verläufen und Fehlerberichten.

Der Schlüssel bestimmt die Organisation. Eine workspace_id im Anfragekörper wird nicht geprüft, sondern ignoriert — sie kann nichts bewirken.

Unbekannt, widerrufen und abgelaufen antworten gleich (401). Wer einen Schlüssel errät, soll nicht erfahren, ob er einmal gültig war.

Rechte

Schreiben schliesst Lesen nicht ein. Wer beides braucht, hakt beides an. So kann ein Schlüssel, der nur Leads einliefert, nicht den ganzen Bestand auslesen.

contacts.readKontakte lesen
contacts.writeKontakte anlegen und ändern
deals.readDeals lesen
deals.writeDeals anlegen und ändern
appointments.readTermine lesen
appointments.writeTermine anlegen, ändern und absagen
activities.readAktivitäten lesen
activities.writeNotizen und Aktivitäten anlegen
calls.readAnrufe lesen (ohne Aufnahmedatei)
pipelines.readPipelines, Phasen, Lead-Status und eigene Felder lesen
users.readNamen der Nutzer lesen (für die Zuordnung von Leads)
webhooks.manageEreignis-Abos anlegen und löschen

Kontakte und Deals lassen sich über die API nicht löschen. Ein gelöschter Kontakt ist endgültig weg; eine Schnittstelle, über die ein Skript das in einer Schleife tun kann, gibt es deshalb nicht.

Regeln, die überall gelten

Blättern

Jede Liste antwortet mit data und next_cursor. Den Zeiger unverändert als ?cursor= zurückschicken; ist er null, ist das Ende erreicht. Keine Seitenzahlen: wird während des Durchlaufs etwas angelegt, würde damit still ein Datensatz durchfallen.

Grenzen

RateLimit-Policy: "minute";q=120;w=60, "tag";q=20000;w=86400
RateLimit: "minute";r=97;t=41, "tag";r=19873;t=52190

120 Aufrufe je Minute, 20.000 je Tag. Schreibende zählen dreifach. Bei Überschreitung kommt 429 mit Retry-After.

Doppelt abgeschickt

Idempotency-Key: bestellung-4711

Bei jedem POST erlaubt. Derselbe Schlüssel innerhalb von 24 Stunden gibt die gespeicherte erste Antwort zurück, ohne etwas anzulegen. Derselbe Schlüssel mit anderem Inhalt gibt 409 — das ist fast immer ein Fehler im aufrufenden Programm.

Version

Innerhalb von v1 kommen nur Felder und Endpunkte dazu. Nichts wird entfernt, kein Typ geändert, kein Feld nachträglich zur Pflicht. Käme je eine v2, stünden Deprecation und Sunset mindestens sechs Monate vorher in jeder Antwort.

Konto

GET/api/v1/mekein Recht nötig

Wer bin ich?

Zeigt Organisation, Rechte und Grenzen des benutzten Schlüssels. Der erste Aufruf, mit dem man eine Anbindung prüft.

Kontakte

GET/api/v1/contactscontacts.read

Kontakte auflisten

Neueste zuerst. `updated_since` holt nur, was sich geändert hat — ohne diesen Filter zieht jeder Abgleich den ganzen Bestand.

Parameter

  • limit (Adresse)1 bis 100, Standard 25.
  • cursor (Adresse)Der Wert aus next_cursor der vorigen Antwort.
  • status (Adresse)Lead-Status, genau. Mögliche Werte über /v1/lead-statuses.
  • owner_id (Adresse)Nur Kontakte dieses Besitzers.
  • email (Adresse)Genaue E-Mail-Adresse.
  • phone (Adresse)Genaue Telefonnummer.
  • query (Adresse)Teiltreffer in Name, E-Mail, Telefon oder Firma.
  • updated_since (Adresse)Nur seitdem Geänderte.
  • created_since (Adresse)Nur seitdem Angelegte.
POST/api/v1/contactscontacts.writeIdempotency-Key

Kontakt anlegen

Gibt es die Person schon (E-Mail, Telefon oder Firmenname), kommt der BESTEHENDE Kontakt zurück — mit `duplicate: true` und Status 200 statt 201.

Anfragekörper

FeldTypGrenzen
first_namePflichtstringmin 1, max 120
last_namestring · darf null seinmax 120
emailstring (email) · darf null seinmax 254
phonestring · darf null seinmax 60
companystring · darf null seinmax 200
statusstring · darf null seinmax 80
sourcefacebook · free_video_course · referral · manual · other
tagsarrayhöchstens 50 Einträge
notesstring · darf null seinmax 5000
owner_idstring (uuid) · darf null sein
assigned_tostring (uuid) · darf null sein
external_idstring · darf null seinmax 200
streetstring · darf null seinmax 200
postal_codestring · darf null seinmax 20
citystring · darf null seinmax 120
countrystring · darf null seinmax 120
websitestring · darf null seinmax 300
custom_dataobject

Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.

curl -X POST https://am-crm.com/api/v1/contacts \
  -H "Authorization: Bearer amk_…" \
  -H "Content-Type: application/json" \
  -d '{"first_name":"Max","last_name":"Mustermann","email":"max@example.com","phone":"+49 170 1234567","company":"Acme GmbH"}'
GET/api/v1/contacts/{id}contacts.read

Einen Kontakt lesen

Ein Kontakt aus einer fremden Organisation antwortet mit 404, nicht 403.

Parameter

  • id (im Pfad)Kennung des Datensatzes.
PATCH/api/v1/contacts/{id}contacts.write

Kontakt ändern

Nur die mitgeschickten Felder. `source` und `external_id` sind nachträglich nicht änderbar — beides beantwortet, woher der Kontakt kam.

Parameter

  • id (im Pfad)Kennung des Datensatzes.

Anfragekörper

FeldTypGrenzen
first_namestringmin 1, max 120
last_namestring · darf null seinmax 120
emailstring (email) · darf null seinmax 254
phonestring · darf null seinmax 60
companystring · darf null seinmax 200
statusstring · darf null seinmax 80
tagsarrayhöchstens 50 Einträge
notesstring · darf null seinmax 5000
owner_idstring (uuid) · darf null sein
assigned_tostring (uuid) · darf null sein
streetstring · darf null seinmax 200
postal_codestring · darf null seinmax 20
citystring · darf null seinmax 120
countrystring · darf null seinmax 120
websitestring · darf null seinmax 300
custom_dataobject

Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.

curl -X PATCH https://am-crm.com/api/v1/contacts/{id} \
  -H "Authorization: Bearer amk_…" \
  -H "Content-Type: application/json" \
  -d '{"status":"Termin vereinbart","notes":"Ruft nächste Woche zurück."}'
GET/api/v1/contacts/{id}/personscontacts.read

Ansprechpartner auflisten

Der Hauptansprechpartner steht oben.

Parameter

  • id (im Pfad)Kennung des Datensatzes.
POST/api/v1/contacts/{id}/personscontacts.write

Ansprechpartner anlegen

`is_primary: true` nimmt dem bisherigen Hauptansprechpartner die Rolle — es gibt genau einen.

Parameter

  • id (im Pfad)Kennung des Datensatzes.

Anfragekörper

FeldTypGrenzen
first_namePflichtstringmin 1, max 120
last_namestring · darf null seinmax 120
salutationstring · darf null seinmax 40
titlestring · darf null seinmax 60
emailstring (email) · darf null seinmax 254
phonestring · darf null seinmax 60
job_titlestring · darf null seinmax 160
rolestring · darf null seinmax 80
senioritystring · darf null seinmax 60
is_primaryboolean
sortinteger≥ 0, ≤ 9999

Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.

curl -X POST https://am-crm.com/api/v1/contacts/{id}/persons \
  -H "Authorization: Bearer amk_…" \
  -H "Content-Type: application/json" \
  -d '{"first_name":"Erika","last_name":"Musterfrau","job_title":"Einkauf","is_primary":true}'
PATCH/api/v1/persons/{id}contacts.write

Ansprechpartner ändern

Die Zugehörigkeit zum Kontakt steht in der Zeile und ändert sich nicht.

Parameter

  • id (im Pfad)Kennung des Datensatzes.

Anfragekörper

FeldTypGrenzen
first_namestringmin 1, max 120
last_namestring · darf null seinmax 120
salutationstring · darf null seinmax 40
titlestring · darf null seinmax 60
emailstring (email) · darf null seinmax 254
phonestring · darf null seinmax 60
job_titlestring · darf null seinmax 160
rolestring · darf null seinmax 80
senioritystring · darf null seinmax 60
is_primaryboolean
sortinteger≥ 0, ≤ 9999

Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.

GET/api/v1/contacts/{id}/activitiesactivities.read

Verlauf lesen

Alle Arten, auch die, die nur das CRM selbst schreibt (`stage_change`, `call`, …).

Parameter

  • id (im Pfad)Kennung des Datensatzes.
  • limit (Adresse)1 bis 100, Standard 25.
  • cursor (Adresse)Der Wert aus next_cursor der vorigen Antwort.
  • type (Adresse)Nur diese Art.
POST/api/v1/contacts/{id}/activitiesactivities.write

Notiz anhängen

Schreibbar sind `note`, `email`, `call`, `appointment`, `task`. `stage_change` und `lead_claim` schreibt nur das CRM selbst — sonst wäre der Verlauf keine Tatsachenaufzeichnung mehr.

Parameter

  • id (im Pfad)Kennung des Datensatzes.

Anfragekörper

FeldTypGrenzen
typenote · email · call · appointment · task
bodyPflichtstringmin 1, max 10000
occurred_atstring (date-time)
external_idstring · darf null seinmax 200

Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.

curl -X POST https://am-crm.com/api/v1/contacts/{id}/activities \
  -H "Authorization: Bearer amk_…" \
  -H "Content-Type: application/json" \
  -d '{"type":"note","body":"Angebot per Post verschickt."}'

Deals

GET/api/v1/dealsdeals.read

Deals auflisten

Neueste zuerst.

Parameter

  • limit (Adresse)1 bis 100, Standard 25.
  • cursor (Adresse)Der Wert aus next_cursor der vorigen Antwort.
  • status (Adresse)open, won, lost oder refunded.
  • pipeline_id (Adresse)Nur diese Pipeline.
  • stage_id (Adresse)Nur diese Phase.
  • contact_id (Adresse)Nur zu diesem Kontakt.
  • owner_id (Adresse)Nur dieses Besitzers.
  • updated_since (Adresse)Nur seitdem Geänderte.
POST/api/v1/dealsdeals.writeIdempotency-Key

Deal anlegen

Beträge in CENT. Wird `stage_id` gesetzt, leitet das CRM den Status aus der Phase ab — ein mitgeschickter `status` wird dann überschrieben, und der Kontakt wandert in diese Phase.

Anfragekörper

FeldTypGrenzen
titlePflichtstringmin 1, max 200
contact_idstring (uuid) · darf null sein
pipeline_idstring (uuid) · darf null sein
stage_idstring (uuid) · darf null sein
value_centsinteger≥ 0, ≤ 2000000000
currencystringmin 3, max 3
statusopen · won · lost · refunded
owner_idstring (uuid) · darf null sein
assigned_tostring (uuid) · darf null sein
product_idstring (uuid) · darf null sein
close_datestring (date) · darf null sein
confidenceinteger · darf null sein≥ 0, ≤ 100
notestring · darf null seinmax 5000
lost_reasonstring · darf null seinmax 500
external_idstring · darf null seinmax 200
custom_dataobject

Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.

curl -X POST https://am-crm.com/api/v1/deals \
  -H "Authorization: Bearer amk_…" \
  -H "Content-Type: application/json" \
  -d '{"title":"Jahreslizenz","contact_id":"…","value_cents":199900,"currency":"EUR"}'
GET/api/v1/deals/{id}deals.read

Einen Deal lesen

Parameter

  • id (im Pfad)Kennung des Datensatzes.
PATCH/api/v1/deals/{id}deals.write

Deal ändern

Auch hier: die Phase bestimmt den Status.

Parameter

  • id (im Pfad)Kennung des Datensatzes.

Anfragekörper

FeldTypGrenzen
titlestringmin 1, max 200
contact_idstring (uuid) · darf null sein
pipeline_idstring (uuid) · darf null sein
stage_idstring (uuid) · darf null sein
value_centsinteger≥ 0, ≤ 2000000000
currencystringmin 3, max 3
statusopen · won · lost · refunded
owner_idstring (uuid) · darf null sein
assigned_tostring (uuid) · darf null sein
product_idstring (uuid) · darf null sein
close_datestring (date) · darf null sein
confidenceinteger · darf null sein≥ 0, ≤ 100
notestring · darf null seinmax 5000
lost_reasonstring · darf null seinmax 500
custom_dataobject

Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.

curl -X PATCH https://am-crm.com/api/v1/deals/{id} \
  -H "Authorization: Bearer amk_…" \
  -H "Content-Type: application/json" \
  -d '{"stage_id":"…"}'

Termine

GET/api/v1/appointmentsappointments.read

Termine auflisten

`from` und `to` grenzen über den Beginn ein.

Parameter

  • limit (Adresse)1 bis 100, Standard 25.
  • cursor (Adresse)Der Wert aus next_cursor der vorigen Antwort.
  • contact_id (Adresse)Nur zu diesem Kontakt.
  • host_id (Adresse)Nur dieses Gastgebers.
  • status (Adresse)booked, confirmed oder cancelled.
  • from (Adresse)Beginn ab.
  • to (Adresse)Beginn bis.
POST/api/v1/appointmentsappointments.writeIdempotency-Key

Termin eintragen

Trägt ein, was anderswo vereinbart wurde. Prüft KEINE freie Zeit und verschickt KEINE Einladung — dafür gibt es die Buchungsseite.

Anfragekörper

FeldTypGrenzen
titlePflichtstringmin 1, max 200
contact_idstring (uuid) · darf null sein
host_idstring (uuid) · darf null sein
event_type_idstring (uuid) · darf null sein
starts_atPflichtstring (date-time)
ends_atPflichtstring (date-time)
timezonestringmax 60
locationstring · darf null seinmax 300
meeting_typestring · darf null seinmax 60
notesstring · darf null seinmax 5000
guest_emailstring (email) · darf null seinmax 254
guest_phonestring · darf null seinmax 60
external_idstring · darf null seinmax 200

Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.

curl -X POST https://am-crm.com/api/v1/appointments \
  -H "Authorization: Bearer amk_…" \
  -H "Content-Type: application/json" \
  -d '{"title":"Erstgespräch","starts_at":"2026-09-10T10:00:00Z","ends_at":"2026-09-10T10:30:00Z"}'
GET/api/v1/appointments/{id}appointments.read

Einen Termin lesen

Parameter

  • id (im Pfad)Kennung des Datensatzes.
PATCH/api/v1/appointments/{id}appointments.write

Termin verlegen oder absagen

`{"cancel": true}` sagt ab — das setzt Status und Zeitpunkt zusammen. Eine Absage lässt sich nicht zurücknehmen: die Gegenseite hat sie längst bekommen.

Parameter

  • id (im Pfad)Kennung des Datensatzes.

Anfragekörper

FeldTypGrenzen
titlestringmin 1, max 200
starts_atstring (date-time)
ends_atstring (date-time)
timezonestringmax 60
locationstring · darf null seinmax 300
meeting_typestring · darf null seinmax 60
notesstring · darf null seinmax 5000
host_idstring (uuid) · darf null sein
canceltrue

Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.

curl -X PATCH https://am-crm.com/api/v1/appointments/{id} \
  -H "Authorization: Bearer amk_…" \
  -H "Content-Type: application/json" \
  -d '{"starts_at":"2026-09-11T14:00:00Z","ends_at":"2026-09-11T14:30:00Z"}'

Anrufe

GET/api/v1/callscalls.read

Anrufe auflisten

Ohne Aufnahme, Abschrift und Rufnummern. Anrufe lassen sich nicht über die API anlegen — sie entstehen aus echten Gesprächen.

Parameter

  • limit (Adresse)1 bis 100, Standard 25.
  • cursor (Adresse)Der Wert aus next_cursor der vorigen Antwort.
  • contact_id (Adresse)Nur zu diesem Kontakt.
  • agent_id (Adresse)Nur dieses Mitarbeiters.
  • direction (Adresse)inbound oder outbound.
  • status (Adresse)completed, no_answer, busy, failed, voicemail …
  • from (Adresse)Beginn ab.
  • to (Adresse)Beginn bis.
GET/api/v1/calls/{id}calls.read

Einen Anruf lesen

Parameter

  • id (im Pfad)Kennung des Datensatzes.

Stammdaten

GET/api/v1/pipelinespipelines.read

Pipelines

Ohne diese Listen kennt ein fremdes System keine einzige Kennung.

GET/api/v1/pipelines/{id}/stagespipelines.read

Phasen einer Pipeline

`is_won` und `is_lost` bestimmen, was ein Deal in dieser Phase bedeutet.

Parameter

  • id (im Pfad)Kennung des Datensatzes.
GET/api/v1/lead-statusespipelines.read

Lead-Status

Die Werte, die `contacts.status` annehmen kann. Je Organisation anders.

Parameter

  • include_archived (Adresse)Auch archivierte zeigen.
GET/api/v1/custom-fieldspipelines.read

Eigene Felder

Welche Schlüssel in `custom_data` erlaubt sind, welchen Typ sie haben.

Parameter

  • entity (Adresse)contact oder deal.
GET/api/v1/usersusers.read

Nutzer

Kennung, Name, Rolle — für die Frage „wem gehört dieser Lead".

Ereignis-Abos

GET/api/v1/webhookswebhooks.manage

Abos auflisten

Liefert auch die Liste aller möglichen Ereignisse mit.

POST/api/v1/webhookswebhooks.manage

Abo anlegen

Antwortet EINMAL mit dem Geheimnis. Damit prüfst du die Unterschrift jeder Zustellung.

Anfragekörper

FeldTypGrenzen
urlPflichtstring (uri)max 500
eventsPflichtarrayhöchstens 20 Einträge
namestringmin 1, max 120
descriptionstring · darf null seinmax 500

Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.

curl -X POST https://am-crm.com/api/v1/webhooks \
  -H "Authorization: Bearer amk_…" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/am-crm","events":["contact.created","deal.won"]}'
PATCH/api/v1/webhooks/{id}webhooks.manage

Abo ändern

`enabled: true` weckt ein stillgelegtes Abo und setzt den Fehlerzähler zurück.

Parameter

  • id (im Pfad)Kennung des Datensatzes.

Anfragekörper

FeldTypGrenzen
urlstring (uri)max 500
eventsarrayhöchstens 20 Einträge
namestringmin 1, max 120
descriptionstring · darf null seinmax 500
enabledboolean

Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.

DELETE/api/v1/webhooks/{id}webhooks.manage

Abo entfernen

Die Zustellprotokolle bleiben stehen.

Parameter

  • id (im Pfad)Kennung des Datensatzes.
POST/api/v1/webhooks/{id}/testwebhooks.manage

Probezustellung

Schickt ein `webhook.test`-Ereignis und gibt die Antwort deines Servers zurück.

Parameter

  • id (im Pfad)Kennung des Datensatzes.

Ereignisse

Statt zu fragen, ob sich etwas geändert hat, lässt du dich benachrichtigen. Abo anlegen über POST /api/v1/webhooks; die Antwort enthält EINMAL das Geheimnis.

contact.createdEin Kontakt wurde angelegt.
contact.updatedEin Kontakt wurde geändert.
contact.status_changedDer Lead-Status eines Kontakts hat sich geändert.
deal.createdEin Deal wurde angelegt.
deal.updatedEin Deal wurde geändert.
deal.wonEin Deal wurde gewonnen.
deal.lostEin Deal wurde verloren.
deal.stage_changedEin Deal ist in eine andere Phase gewandert.
appointment.bookedEin Termin wurde gebucht.
appointment.rescheduledEin Termin wurde verlegt.
appointment.cancelledEin Termin wurde abgesagt.
call.completedEin Anruf ist beendet.

Unterschrift prüfen

Nach Standard Webhooks — dafür gibt es fertige Bibliotheken. Von Hand:

// Kopfzeilen: webhook-id, webhook-timestamp, webhook-signature
const inhalt = `${id}.${zeitstempel}.${rohkoerper}`;
const erwartet = 'v1,' + crypto
  .createHmac('sha256', Buffer.from(geheimnis.slice(6), 'base64'))
  .update(inhalt).digest('base64');
// Zeitstempel älter als 5 Minuten: ablehnen (Schutz vor Wiedereinspielung).

Wichtig: über den ROHEN Körper unterschreiben, nicht über neu verpacktes JSON. Antworte mit 2xx; alles andere gilt als Fehlschlag und wird bis zu dreimal wiederholt. Benutze webhook-id als Idempotenz-Schlüssel — Zustellungen können sich wiederholen.

Fremde Apps — OAuth 2.1

Ein API-Schlüssel gehört der Organisation und wird von ihrem Inhaber in sein eigenes System getragen. Baust du eine App, die sich Nutzer verbinden, nimm OAuth: dann sieht der Nutzer, wer was will, sagt einmal ja und kann genau deiner App den Zugang wieder nehmen.

Einrichten

Deine App kann sich selbst registrieren — kein Antrag, kein Warten:

curl -X POST https://am-crm.com/api/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name":"Meine App",
       "redirect_uris":["https://meine-app.de/oauth/callback"],
       "scope":"contacts.read contacts.write"}'

Antwort: deine client_id. Ein client_secret gibt es nicht — selbst registrierte Apps sind öffentlich und sichern sich über PKCE. Alle Adressen stehen maschinenlesbar unter /.well-known/oauth-authorization-server.

Der Ablauf

1. PKCE erzeugen
   verifier  = 43-128 zufällige Zeichen
   challenge = base64url(sha256(verifier))

2. Nutzer hinschicken
   https://am-crm.com/oauth/authorize
     ?client_id=amc_…
     &redirect_uri=https://meine-app.de/oauth/callback   (exakt wie registriert)
     &response_type=code
     &scope=contacts.read+contacts.write
     &state=<eigener Zufallswert>
     &code_challenge=<challenge>
     &code_challenge_method=S256

3. Rücksprung mit ?code=…&state=…   (state vergleichen!)

4. Code eintauschen
   POST https://am-crm.com/api/oauth/token
     grant_type=authorization_code
     client_id=amc_…  code=…  code_verifier=<verifier>
     redirect_uri=…

   → { "access_token": "amo_…", "expires_in": 3600,
       "refresh_token": "amr_…", "scope": "…" }

5. Aufrufen wie mit einem Schlüssel
   Authorization: Bearer amo_…

Was du beachten musst

  • PKCE ist Pflicht, nur S256. plain wird abgelehnt.
  • Ein Code, ein Mal. Ein zweites Einlösen widerruft, was aus ihm entstand — das ist der Schutz gegen abgefangene Codes.
  • Jedes Erneuern dreht das Token weiter. Das alte Paar stirbt sofort. Benutzt du ein altes refresh_token ein zweites Mal, wird der ganze Zugang widerrufen und der Nutzer muss erneut zustimmen — speichere also immer das zuletzt erhaltene.
  • Rücksprung-Adressen müssen exakt stimmen. Zeichen für Zeichen, kein Präfix. https ist Pflicht; http://localhost und eigene App-Schemata sind erlaubt.
  • Zugang beenden: POST /api/oauth/revoke mit token=… (RFC 7009).

KI-Agenten — MCP

Unter https://am-crm.com/api/mcp steht ein MCP-Server. Er ist eine Ansicht auf genau diese API: jeder Werkzeugaufruf geht durch dieselbe Route, dieselben Rechte, dieselbe Ratenzählung. Ein Agent kann damit nichts, was der Zugang nicht ohnehin dürfte.

{
  "mcpServers": {
    "am-crm": {
      "url": "https://am-crm.com/api/mcp",
      "headers": { "Authorization": "Bearer amk_…" }
    }
  }
}

Clients, die OAuth können, brauchen den Schlüssel nicht: sie bekommen auf einen Aufruf ohne Anmeldung eine 401 mit WWW-Authenticate, finden darüber den Autorisierungsserver und führen den Nutzer durch die Zustimmung.

tools/list zeigt nur, was dieser Zugang wirklich darf — ein Werkzeug anzubieten, das dann mit 403 antwortet, kostet einen Aufruf und eine Erklärung.

Fehler

Immer application/problem+json nach RFC 9457.

{
  "type": "https://am-crm.com/docs/api/errors/validation-failed",
  "title": "Die Eingabe ist unvollständig oder falsch.",
  "status": 422,
  "code": "validation_failed",
  "detail": "first_name: darf nicht leer sein",
  "instance": "/api/v1/contacts",
  "request_id": "req_9f2c…",
  "errors": [
    {
      "field": "first_name",
      "message": "darf nicht leer sein"
    }
  ]
}
unauthorizedKein gültiger Schlüssel.
forbidden_scopeDem Schlüssel fehlt das Recht dafür.
not_foundNicht gefunden.
validation_failedDie Eingabe ist unvollständig oder falsch.
invalid_jsonDer Anfragekörper ist kein gültiges JSON.
idempotency_conflictDerselbe Idempotency-Key wurde schon mit einem anderen Inhalt benutzt.
quota_exceededZu viele Anfragen.
method_not_allowedDiese Methode gibt es hier nicht.
unsupported_media_typeEs wird application/json erwartet.
payload_too_largeDer Anfragekörper ist zu gross.
conflictDer Datensatz hat sich zwischenzeitlich geändert.
server_errorBei uns ist etwas schiefgegangen.
service_unavailableGerade nicht verfügbar.

Jede Antwort trägt X-Request-Id. Nenne diese Kennung bei einer Störung — damit finden wir den Aufruf im Protokoll wieder, das auch du in den Einstellungen siehst.