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.
2. Provide the token
Store the token in the environment variable M2H_API_TOKEN — never directly in the source code.
$env:M2H_API_TOKEN = "m2h_live_…"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 -s "https://api.manage2hero.com/v1/locations" \
-H "Authorization: Bearer $M2H_API_TOKEN"$headers = @{ Authorization = "Bearer $env:M2H_API_TOKEN" }
Invoke-RestMethod -Uri "https://api.manage2hero.com/v1/locations" -Headers $headersconst 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 hereimport 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)
{
"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
| HTTP | error.code | Cause | Fix |
|---|---|---|---|
| 401 | missing_token | The Authorization header is missing or empty (often: the environment variable is not set). | Send Authorization: Bearer <token>; check the variable. |
| 401 | invalid_token | Token copied wrongly (spaces, quotation marks), revoked, or its creator was deactivated. | Copy the token again; otherwise create a new one in the management app. |
| 401 | token_expired | The validity period of the token has ended. | Create a new token and replace it. |
| 401 | wrong_environment | A 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). |
| 403 | insufficient_scope | The 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. |
| 403 | insufficient_permissions | The 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. |
| 429 | rate_limited | Too many calls in a short time. | Wait for Retry-After, then retry (see limits). |
{
"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:
- 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 -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"}'$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 $bodyimport { 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 hereimport 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"])- 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 withPOST /v1/webhooks/{id}/rotate-secret. - Test it:
POST /v1/webhooks/{id}/test(withIdempotency-Key) queues a test eventwebhook.test; the server sends it within about a minute.
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)"- Check the delivery status:
GET /v1/webhooks/{id}/deliveriesshows per deliverystatus(pending,succeeded,failed,abandoned,paused),response_status(the status code your receiver answered) and a fixederrorcode.succeededwithresponse_status: 200means 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.
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.