Skip to content

Quickstart in five minutes

Want to try it first, without real data? The sandbox offers made-up sample data and its own test tokens (m2h_test_…) — usable while API access is active (including the free trial), with webhooks and event simulation by example call.

Prerequisites

  • The API is enabled for your organization (trial or booked). Administration → API access shows the status; without it no token can be created.
  • You have the permission API access with full access in the management app (only that allows creating tokens) and every permission the token is going to get yourself.
  • A computer or server with internet access that can send https requests (curl is enough). The API is meant for server-to-server calls, not for browsers.

1. Create a token

In the management app: Administration → API access → New token. Enter a name (e.g. “Quickstart”), choose exactly one scope as the permission — for this quickstart locations:read (“read garage parks”; the creator needs the permission Locations with view access for it) — and choose a validity period. More scopes: permissions.

The token is shown only once. Copy it straight into a password manager or the secret store of your program.

2. Provide the token

Store the token in the environment variable M2H_API_TOKEN — never directly in the source code.

PowerShell
$env:M2H_API_TOKEN = "m2h_live_…"
Linux / macOS
export M2H_API_TOKEN="m2h_live_…"

3. First request: your garage parks

The curl examples use Bash syntax ($VARIABLE, \ at the end of a line, $(uuidgen)): they run on Linux, macOS and Git Bash. On Windows use the PowerShell example right below.

curl (Bash: Linux, macOS, Git Bash)
curl -s "https://api.manage2hero.com/v1/locations" \
  -H "Authorization: Bearer $M2H_API_TOKEN"
PowerShell (Windows)
$headers = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/locations" -Headers $headers
JavaScript (Node.js 18+)
const res = await fetch('https://api.manage2hero.com/v1/locations', {
  headers: { Authorization: `Bearer ${process.env.M2H_API_TOKEN}` },
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const data = body.data; // continue processing here
Python (requests)
import os, requests

res = requests.get(
    "https://api.manage2hero.com/v1/locations",
    headers={"Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}"},
    timeout=30,
)
body = res.json()
if not body["success"]:
    raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])

Response (200, complete)

JSON
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "6a1f0c2e-4d7b-4b8e-9f10-2c3d4e5f6a7b",
        "code": "HH01",
        "name": "Garagenpark Hamburg Nord",
        "street": "Beispielweg",
        "house_number": "12",
        "postal_code": "22297",
        "city": "Hamburg",
        "country_code": "DE",
        "active": true
      }
    ],
    "next_cursor": null
  },
  "request_id": "req_4a2c9e1b7e84d2f9c0a6b3d5e7f1a2c9"
}

items holds one object per garage park; without any garage park the list is empty ([]). next_cursor is null when there is no further page. With the id of a garage park you can then fetch, for example, its occupancy rate: GET /v1/locations/{id}/occupancy.

Typical errors on the first request

HTTPerror.codeCauseFix
401missing_tokenThe Authorization header is missing or empty (often: the environment variable is not set).Send Authorization: Bearer <token>; check the variable.
401invalid_tokenToken copied wrongly (spaces, quotation marks), revoked, or its creator was deactivated.Copy the token again; otherwise create a new one in the management app.
401token_expiredThe validity period of the token has ended.Create a new token and replace it.
401wrong_environmentA token of the stg or dev environment was used against production (or the other way round).Use the token of the matching environment (live, stg, dev).
403insufficient_scopeThe token lacks the scope of the endpoint; error.required_scope names it.Create a new token with that scope — an existing one cannot be extended.
403insufficient_permissionsThe token creator does not (or no longer) have the required permission in the management app.Add the permission; the token then works again without any change.
429rate_limitedToo many calls in a short time.Wait for Retry-After, then retry (see limits).
403: missing scope
{
  "success": false,
  "error": {
    "code": "insufficient_scope",
    "message": "<explanatory text in German>",
    "required_scope": "locations:read"
  },
  "request_id": "req_8c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f"
}

All error codes with advice on what to do: Error codes; volume limits: Limits.

4. Optional: receive events by webhook

Instead of polling regularly, Manage²Hero reports events (payment, dunning notice, contract, invoice, document, email, damage report) to your system as a signed POST. The shortest path:

  1. Create the webhook: a new token with webhooks:manage (creator: API access with full access) and the read scopes of the events you subscribe to. Your address must be a publicly reachable https address on port 443.
curl (Bash: Linux, macOS, Git Bash)
curl -s -X POST "https://api.manage2hero.com/v1/webhooks" \
  -H "Authorization: Bearer $M2H_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data '{"url":"https://erp.example.com/hooks/manage2hero","events":["payment.received","invoice.created"],"description":"ERP Buchhaltung"}'
PowerShell (Windows)
$headers = @{
  Authorization     = "Bearer $env:M2H_API_TOKEN"
  "Idempotency-Key" = [guid]::NewGuid().ToString()
}
$body = '{"url":"https://erp.example.com/hooks/manage2hero","events":["payment.received","invoice.created"],"description":"ERP Buchhaltung"}'
Invoke-RestMethod -Method Post -Uri "https://api.manage2hero.com/v1/webhooks" -Headers $headers -ContentType "application/json; charset=utf-8" -Body $body
JavaScript (Node.js 18+)
import { createHash, randomUUID } from 'node:crypto';
import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';

// Generate the key ONCE and store it durably BEFORE sending (here: a file, one per operation). After a
// restart the same key is read. For a new, different operation choose a new file name. Delete the file only after the final response.
const VORGANG = 'auftrag-2026/1001'; // set YOUR OWN id per operation (e.g. order id); never reuse it for two operations
const VORGANG_ID = createHash('sha256').update(VORGANG).digest('hex').slice(0, 16); // hashed for the file name: any characters (/, \ …) in the id are harmless
const KEY_FILE = `idempotency-key-post-v1-webhooks-${VORGANG_ID}.txt`;
const neu = randomUUID();
const tmp = `${KEY_FILE}.${neu}`; // unique name per attempt, never overwrite an existing file
writeFileSync(tmp, neu, { flag: 'wx' }); // write it completely to a temporary file first
try {
  linkSync(tmp, KEY_FILE); // publishes it atomically without overwriting an existing one
} catch (e) {
  if (e.code !== 'EEXIST') throw e; // an existing key stays unchanged
} finally {
  unlinkSync(tmp);
}
const idempotencyKey = readFileSync(KEY_FILE, 'utf8').trim();

const res = await fetch('https://api.manage2hero.com/v1/webhooks', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.M2H_API_TOKEN}`,
    'Content-Type': 'application/json',
    // On every retry (e.g. after a timeout) send this stored key again.
    'Idempotency-Key': idempotencyKey,
  },
  body: JSON.stringify({
    "url": "https://erp.example.com/hooks/manage2hero",
    "events": [
      "payment.received",
      "invoice.created"
    ],
    "description": "ERP Buchhaltung"
  }),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
const data = body.data; // continue processing here
Python (requests)
import hashlib, os, uuid, requests
from pathlib import Path

# Generate the key ONCE and store it durably BEFORE sending (here: a file, one per operation). After a
# restart the same key is read. For a new, different operation choose a new file name. Delete the file only after the final response.
VORGANG = "auftrag-2026/1001"  # set YOUR OWN id per operation (e.g. order id); never reuse it for two operations
VORGANG_ID = hashlib.sha256(VORGANG.encode()).hexdigest()[:16]  # hashed for the file name: any characters (/, \ …) in the id are harmless
KEY_FILE = Path(f"idempotency-key-post-v1-webhooks-{VORGANG_ID}.txt")
neu = str(uuid.uuid4())
tmp = Path(f"{KEY_FILE}.{neu}")  # unique name per attempt, never overwrite an existing file
with tmp.open("x") as f:
    f.write(neu)  # write it completely to a temporary file first
try:
    os.link(tmp, KEY_FILE)  # publishes it atomically without overwriting an existing one
except FileExistsError:
    pass  # an existing key stays unchanged
finally:
    tmp.unlink()
idempotency_key = KEY_FILE.read_text().strip()

res = requests.post(
    "https://api.manage2hero.com/v1/webhooks",
    headers={
        "Authorization": f"Bearer {os.environ['M2H_API_TOKEN']}",
        "Idempotency-Key": idempotency_key,
    },
    json={"url":"https://erp.example.com/hooks/manage2hero","events":["payment.received","invoice.created"],"description":"ERP Buchhaltung"},
    timeout=30,
)
body = res.json()
if not body["success"]:
    raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"])
  1. Store the secret: the response (201) contains the secret (whsec_…) once. Put it into the secret store of your receiver right away. Lost? Create a new one with POST /v1/webhooks/{id}/rotate-secret.
  2. Test it: POST /v1/webhooks/{id}/test (with Idempotency-Key) queues a test event webhook.test; the server sends it within about a minute.
curl
curl -s -X POST "https://api.manage2hero.com/v1/webhooks/3f6c1b52-8a47-4d09-9e1c-5b2a7d4e6f80/test" \
  -H "Authorization: Bearer $M2H_API_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
  1. Check the delivery status: GET /v1/webhooks/{id}/deliveries shows per delivery status (pending, succeeded, failed, abandoned, paused), response_status (the status code your receiver answered) and a fixed error code. succeeded with response_status: 200 means your receiver works. A message is tried up to 6 times; if the last attempt fails, the webhook is disabled (active: false). The delivery history is kept for 30 days.
Messages are only sent from production; in development and staging deliveries stay open (pending). Verifying the signature, retries, disabling and operation: the webhooks guide. A runnable receiver project (signature check, inbox table, reconciliation) is available as a ZIP download (no sign-in, example code under the MIT license). The messages are described in the openapi.json in the section webhooks (not under paths; important for code generators). The most important rules for operation are summarized in the integration checklist. All calls to try out are available as a Postman collection.