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.read | Kontakte lesen |
contacts.write | Kontakte anlegen und ändern |
deals.read | Deals lesen |
deals.write | Deals anlegen und ändern |
appointments.read | Termine lesen |
appointments.write | Termine anlegen, ändern und absagen |
activities.read | Aktivitäten lesen |
activities.write | Notizen und Aktivitäten anlegen |
calls.read | Anrufe lesen (ohne Aufnahmedatei) |
pipelines.read | Pipelines, Phasen, Lead-Status und eigene Felder lesen |
users.read | Namen der Nutzer lesen (für die Zuordnung von Leads) |
webhooks.manage | Ereignis-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=52190120 Aufrufe je Minute, 20.000 je Tag. Schreibende zählen dreifach. Bei Überschreitung kommt 429 mit Retry-After.
Doppelt abgeschickt
Idempotency-Key: bestellung-4711Bei 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
/api/v1/mekein Recht nötigWer bin ich?
Zeigt Organisation, Rechte und Grenzen des benutzten Schlüssels. Der erste Aufruf, mit dem man eine Anbindung prüft.
Kontakte
/api/v1/contactscontacts.readKontakte 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.
/api/v1/contactscontacts.writeIdempotency-KeyKontakt 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
| Feld | Typ | Grenzen |
|---|---|---|
first_namePflicht | string | min 1, max 120 |
last_name | string · darf null sein | max 120 |
email | string (email) · darf null sein | max 254 |
phone | string · darf null sein | max 60 |
company | string · darf null sein | max 200 |
status | string · darf null sein | max 80 |
source | facebook · free_video_course · referral · manual · other | — |
tags | array | höchstens 50 Einträge |
notes | string · darf null sein | max 5000 |
owner_id | string (uuid) · darf null sein | — |
assigned_to | string (uuid) · darf null sein | — |
external_id | string · darf null sein | max 200 |
street | string · darf null sein | max 200 |
postal_code | string · darf null sein | max 20 |
city | string · darf null sein | max 120 |
country | string · darf null sein | max 120 |
website | string · darf null sein | max 300 |
custom_data | object | — |
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"}'/api/v1/contacts/{id}contacts.readEinen Kontakt lesen
Ein Kontakt aus einer fremden Organisation antwortet mit 404, nicht 403.
Parameter
id(im Pfad) — Kennung des Datensatzes.
/api/v1/contacts/{id}contacts.writeKontakt ä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
| Feld | Typ | Grenzen |
|---|---|---|
first_name | string | min 1, max 120 |
last_name | string · darf null sein | max 120 |
email | string (email) · darf null sein | max 254 |
phone | string · darf null sein | max 60 |
company | string · darf null sein | max 200 |
status | string · darf null sein | max 80 |
tags | array | höchstens 50 Einträge |
notes | string · darf null sein | max 5000 |
owner_id | string (uuid) · darf null sein | — |
assigned_to | string (uuid) · darf null sein | — |
street | string · darf null sein | max 200 |
postal_code | string · darf null sein | max 20 |
city | string · darf null sein | max 120 |
country | string · darf null sein | max 120 |
website | string · darf null sein | max 300 |
custom_data | object | — |
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."}'/api/v1/contacts/{id}/personscontacts.readAnsprechpartner auflisten
Der Hauptansprechpartner steht oben.
Parameter
id(im Pfad) — Kennung des Datensatzes.
/api/v1/contacts/{id}/personscontacts.writeAnsprechpartner anlegen
`is_primary: true` nimmt dem bisherigen Hauptansprechpartner die Rolle — es gibt genau einen.
Parameter
id(im Pfad) — Kennung des Datensatzes.
Anfragekörper
| Feld | Typ | Grenzen |
|---|---|---|
first_namePflicht | string | min 1, max 120 |
last_name | string · darf null sein | max 120 |
salutation | string · darf null sein | max 40 |
title | string · darf null sein | max 60 |
email | string (email) · darf null sein | max 254 |
phone | string · darf null sein | max 60 |
job_title | string · darf null sein | max 160 |
role | string · darf null sein | max 80 |
seniority | string · darf null sein | max 60 |
is_primary | boolean | — |
sort | integer | ≥ 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}'/api/v1/persons/{id}contacts.writeAnsprechpartner ändern
Die Zugehörigkeit zum Kontakt steht in der Zeile und ändert sich nicht.
Parameter
id(im Pfad) — Kennung des Datensatzes.
Anfragekörper
| Feld | Typ | Grenzen |
|---|---|---|
first_name | string | min 1, max 120 |
last_name | string · darf null sein | max 120 |
salutation | string · darf null sein | max 40 |
title | string · darf null sein | max 60 |
email | string (email) · darf null sein | max 254 |
phone | string · darf null sein | max 60 |
job_title | string · darf null sein | max 160 |
role | string · darf null sein | max 80 |
seniority | string · darf null sein | max 60 |
is_primary | boolean | — |
sort | integer | ≥ 0, ≤ 9999 |
Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.
/api/v1/contacts/{id}/activitiesactivities.readVerlauf 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.
/api/v1/contacts/{id}/activitiesactivities.writeNotiz 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
| Feld | Typ | Grenzen |
|---|---|---|
type | note · email · call · appointment · task | — |
bodyPflicht | string | min 1, max 10000 |
occurred_at | string (date-time) | — |
external_id | string · darf null sein | max 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
/api/v1/dealsdeals.readDeals 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.
/api/v1/dealsdeals.writeIdempotency-KeyDeal 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
| Feld | Typ | Grenzen |
|---|---|---|
titlePflicht | string | min 1, max 200 |
contact_id | string (uuid) · darf null sein | — |
pipeline_id | string (uuid) · darf null sein | — |
stage_id | string (uuid) · darf null sein | — |
value_cents | integer | ≥ 0, ≤ 2000000000 |
currency | string | min 3, max 3 |
status | open · won · lost · refunded | — |
owner_id | string (uuid) · darf null sein | — |
assigned_to | string (uuid) · darf null sein | — |
product_id | string (uuid) · darf null sein | — |
close_date | string (date) · darf null sein | — |
confidence | integer · darf null sein | ≥ 0, ≤ 100 |
note | string · darf null sein | max 5000 |
lost_reason | string · darf null sein | max 500 |
external_id | string · darf null sein | max 200 |
custom_data | object | — |
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"}'/api/v1/deals/{id}deals.readEinen Deal lesen
Parameter
id(im Pfad) — Kennung des Datensatzes.
/api/v1/deals/{id}deals.writeDeal ändern
Auch hier: die Phase bestimmt den Status.
Parameter
id(im Pfad) — Kennung des Datensatzes.
Anfragekörper
| Feld | Typ | Grenzen |
|---|---|---|
title | string | min 1, max 200 |
contact_id | string (uuid) · darf null sein | — |
pipeline_id | string (uuid) · darf null sein | — |
stage_id | string (uuid) · darf null sein | — |
value_cents | integer | ≥ 0, ≤ 2000000000 |
currency | string | min 3, max 3 |
status | open · won · lost · refunded | — |
owner_id | string (uuid) · darf null sein | — |
assigned_to | string (uuid) · darf null sein | — |
product_id | string (uuid) · darf null sein | — |
close_date | string (date) · darf null sein | — |
confidence | integer · darf null sein | ≥ 0, ≤ 100 |
note | string · darf null sein | max 5000 |
lost_reason | string · darf null sein | max 500 |
custom_data | object | — |
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
/api/v1/appointmentsappointments.readTermine 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.
/api/v1/appointmentsappointments.writeIdempotency-KeyTermin 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
| Feld | Typ | Grenzen |
|---|---|---|
titlePflicht | string | min 1, max 200 |
contact_id | string (uuid) · darf null sein | — |
host_id | string (uuid) · darf null sein | — |
event_type_id | string (uuid) · darf null sein | — |
starts_atPflicht | string (date-time) | — |
ends_atPflicht | string (date-time) | — |
timezone | string | max 60 |
location | string · darf null sein | max 300 |
meeting_type | string · darf null sein | max 60 |
notes | string · darf null sein | max 5000 |
guest_email | string (email) · darf null sein | max 254 |
guest_phone | string · darf null sein | max 60 |
external_id | string · darf null sein | max 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"}'/api/v1/appointments/{id}appointments.readEinen Termin lesen
Parameter
id(im Pfad) — Kennung des Datensatzes.
/api/v1/appointments/{id}appointments.writeTermin 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
| Feld | Typ | Grenzen |
|---|---|---|
title | string | min 1, max 200 |
starts_at | string (date-time) | — |
ends_at | string (date-time) | — |
timezone | string | max 60 |
location | string · darf null sein | max 300 |
meeting_type | string · darf null sein | max 60 |
notes | string · darf null sein | max 5000 |
host_id | string (uuid) · darf null sein | — |
cancel | true | — |
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
/api/v1/callscalls.readAnrufe 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.
/api/v1/calls/{id}calls.readEinen Anruf lesen
Parameter
id(im Pfad) — Kennung des Datensatzes.
Stammdaten
/api/v1/pipelinespipelines.readPipelines
Ohne diese Listen kennt ein fremdes System keine einzige Kennung.
/api/v1/pipelines/{id}/stagespipelines.readPhasen einer Pipeline
`is_won` und `is_lost` bestimmen, was ein Deal in dieser Phase bedeutet.
Parameter
id(im Pfad) — Kennung des Datensatzes.
/api/v1/lead-statusespipelines.readLead-Status
Die Werte, die `contacts.status` annehmen kann. Je Organisation anders.
Parameter
include_archived(Adresse) — Auch archivierte zeigen.
/api/v1/custom-fieldspipelines.readEigene Felder
Welche Schlüssel in `custom_data` erlaubt sind, welchen Typ sie haben.
Parameter
entity(Adresse) — contact oder deal.
/api/v1/usersusers.readNutzer
Kennung, Name, Rolle — für die Frage „wem gehört dieser Lead".
Ereignis-Abos
/api/v1/webhookswebhooks.manageAbos auflisten
Liefert auch die Liste aller möglichen Ereignisse mit.
/api/v1/webhookswebhooks.manageAbo anlegen
Antwortet EINMAL mit dem Geheimnis. Damit prüfst du die Unterschrift jeder Zustellung.
Anfragekörper
| Feld | Typ | Grenzen |
|---|---|---|
urlPflicht | string (uri) | max 500 |
eventsPflicht | array | höchstens 20 Einträge |
name | string | min 1, max 120 |
description | string · darf null sein | max 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"]}'/api/v1/webhooks/{id}webhooks.manageAbo ändern
`enabled: true` weckt ein stillgelegtes Abo und setzt den Fehlerzähler zurück.
Parameter
id(im Pfad) — Kennung des Datensatzes.
Anfragekörper
| Feld | Typ | Grenzen |
|---|---|---|
url | string (uri) | max 500 |
events | array | höchstens 20 Einträge |
name | string | min 1, max 120 |
description | string · darf null sein | max 500 |
enabled | boolean | — |
Unbekannte Felder werden abgewiesen, nicht ignoriert — ein Tippfehler im Feldnamen fällt damit sofort auf.
/api/v1/webhooks/{id}webhooks.manageAbo entfernen
Die Zustellprotokolle bleiben stehen.
Parameter
id(im Pfad) — Kennung des Datensatzes.
/api/v1/webhooks/{id}/testwebhooks.manageProbezustellung
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.created | Ein Kontakt wurde angelegt. |
contact.updated | Ein Kontakt wurde geändert. |
contact.status_changed | Der Lead-Status eines Kontakts hat sich geändert. |
deal.created | Ein Deal wurde angelegt. |
deal.updated | Ein Deal wurde geändert. |
deal.won | Ein Deal wurde gewonnen. |
deal.lost | Ein Deal wurde verloren. |
deal.stage_changed | Ein Deal ist in eine andere Phase gewandert. |
appointment.booked | Ein Termin wurde gebucht. |
appointment.rescheduled | Ein Termin wurde verlegt. |
appointment.cancelled | Ein Termin wurde abgesagt. |
call.completed | Ein 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.plainwird 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_tokenein 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.
httpsist Pflicht;http://localhostund eigene App-Schemata sind erlaubt. - Zugang beenden:
POST /api/oauth/revokemittoken=…(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"
}
]
}unauthorized | Kein gültiger Schlüssel. |
forbidden_scope | Dem Schlüssel fehlt das Recht dafür. |
not_found | Nicht gefunden. |
validation_failed | Die Eingabe ist unvollständig oder falsch. |
invalid_json | Der Anfragekörper ist kein gültiges JSON. |
idempotency_conflict | Derselbe Idempotency-Key wurde schon mit einem anderen Inhalt benutzt. |
quota_exceeded | Zu viele Anfragen. |
method_not_allowed | Diese Methode gibt es hier nicht. |
unsupported_media_type | Es wird application/json erwartet. |
payload_too_large | Der Anfragekörper ist zu gross. |
conflict | Der Datensatz hat sich zwischenzeitlich geändert. |
server_error | Bei uns ist etwas schiefgegangen. |
service_unavailable | Gerade 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.