Google Business connect sessions — create, read and list
A connect session is one end user’s trip through the hosted connect flow: you create it, send the client to
its url, and read the outcome. The whole workflow, with samples, is in
Connect your clients’ Google Business Profiles. The addresses a session may
return to are managed with return addresses.
These endpoints need a package that includes the connector (402 gmb_plan_required otherwise).
The session object
Section titled “The session object”{ "id": "gms_7d2q9m4k1x", "state": "completed", "externalId": "client-4821", "connectionId": "gmc_5p1x8c3m7z", "error": null, "expiresAt": "2026-10-14T02:30:00.000Z", "createdAt": "2026-10-14T02:00:00.000Z", "completedAt": "2026-10-14T02:03:41.000Z"}| Field | Meaning |
|---|---|
id |
The session id (gms_…). Store it against your own client record |
state |
pending, completed, cancelled, failed or expired. expired is a pending session whose expiresAt has passed: its link no longer works, and the customer creates a new session |
externalId |
Your own id for the end user, as you passed it |
connectionId |
The connection (gmc_…) created, once completed; otherwise null |
error |
A symbolic gmb_* code when failed; otherwise null. Never Google’s wording |
expiresAt |
When the link stops working: 30 minutes after creation |
createdAt, completedAt |
completedAt is set when the session leaves pending (completed, cancelled or failed) |
url |
Only in the response that creates the session. The link to send the end user to. A secret: it is never returned again, and only its hash is stored |
POST /gmb/connect-sessions
Section titled “POST /gmb/connect-sessions”Creates a session.
| Scope | gmb |
| Body | application/json |
| Rate limit | 120 sessions per hour per account |
| Field | Required | Rule |
|---|---|---|
externalId |
yes | 1–128 characters of letters, digits and _ . : @ + - |
redirectUrl |
yes | https (or http on localhost), no credentials, no fragment, up to 2,000 characters, inside a registered return address. A query string is allowed and is kept; the connect flow adds maukirim_session, status, external_id, connection_id and error to it |
label |
no | Up to 80 characters; the connection’s label. Blank is ignored |
The checks run in this order: the package, externalId, redirectUrl against the allow-list, then the rate
limit. A refusal creates nothing.
curl -X POST https://app.maukirim.com/api/v1/gmb/connect-sessions \ -H "Authorization: Bearer mk_live_…" \ -H "Content-Type: application/json" \ -d '{ "externalId": "client-4821", "redirectUrl": "https://app.example.com/google/return?ref=4821", "label": "Kopi Satu" }'const response = await fetch("https://app.maukirim.com/api/v1/gmb/connect-sessions", { method: "POST", headers: { Authorization: `Bearer ${process.env.MAUKIRIM_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ externalId: "client-4821", redirectUrl: "https://app.example.com/google/return?ref=4821", label: "Kopi Satu", }),})const { session } = await response.json() // session.url is the link; keep it out of your logs$curl = curl_init('https://app.maukirim.com/api/v1/gmb/connect-sessions');curl_setopt_array($curl, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('MAUKIRIM_KEY'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'externalId' => 'client-4821', 'redirectUrl' => 'https://app.example.com/google/return?ref=4821', 'label' => 'Kopi Satu', ]), CURLOPT_RETURNTRANSFER => true,]);$session = json_decode(curl_exec($curl), true)['session'];201 Created:
{ "ok": true, "session": { "id": "gms_7d2q9m4k1x", "state": "pending", "externalId": "client-4821", "connectionId": null, "error": null, "expiresAt": "2026-10-14T02:30:00.000Z", "createdAt": "2026-10-14T02:00:00.000Z", "completedAt": null, "url": "https://app.maukirim.com/connect/gmb/gcs_Tq8…" }}Creating a session is not idempotent: there is no Idempotency-Key and every call makes a new link.
What the redirect carries
Section titled “What the redirect carries”When the trip ends the browser is sent to redirectUrl with these parameters added:
| Parameter | Value |
|---|---|
maukirim_session |
The session id |
status |
success, cancelled or error |
external_id |
Your externalId |
connection_id |
On success only |
error |
access_denied for cancelled; a gmb_* code for error |
It never carries a token, the link or any Google identifier. Read the session to confirm the result instead of trusting the query string.
Errors
Section titled “Errors”| HTTP | Code | Condition |
|---|---|---|
| 400 | invalid_input |
The body is not a JSON object, or externalId is missing or outside its pattern |
| 400 | gmb_redirect_not_allowed |
redirectUrl is missing, not absolute, not https (or localhost http), has credentials or a fragment, or is not inside a registered return address |
| 401 | api_key_invalid / api_key_revoked / api_key_expired |
|
| 402 | gmb_plan_required |
The package does not include the connector |
| 403 | api_key_scope_denied |
The key lacks gmb |
| 429 | rate_limited |
More than 120 sessions in an hour. Carries Retry-After and retryAfterSeconds |
GET /gmb/connect-sessions/{id}
Section titled “GET /gmb/connect-sessions/{id}”One session. It never contains url.
| Scope | read or gmb |
| Path | id — the session id (gms_…) |
curl https://app.maukirim.com/api/v1/gmb/connect-sessions/gms_7d2q9m4k1x -H "Authorization: Bearer mk_live_…"const response = await fetch(`https://app.maukirim.com/api/v1/gmb/connect-sessions/${sessionId}`, { headers: { Authorization: `Bearer ${process.env.MAUKIRIM_KEY}` },})const { session } = await response.json()if (session.state === "completed") console.log(session.connectionId)$curl = curl_init("https://app.maukirim.com/api/v1/gmb/connect-sessions/$sessionId");curl_setopt_array($curl, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('MAUKIRIM_KEY')], CURLOPT_RETURNTRANSFER => true,]);$session = json_decode(curl_exec($curl), true)['session'];{ "ok": true, "session": { "id": "gms_7d2q9m4k1x", "state": "failed", "externalId": "client-4821", "connectionId": null, "error": "gmb_authorization_failed", "expiresAt": "2026-10-14T02:30:00.000Z", "createdAt": "2026-10-14T02:00:00.000Z", "completedAt": "2026-10-14T02:03:41.000Z" }}No Google call and no budget.
Errors
Section titled “Errors”| HTTP | Code | Condition |
|---|---|---|
| 402 | gmb_plan_required |
The package does not include the connector |
| 403 | api_key_scope_denied |
Neither read nor gmb |
| 404 | gmb_session_not_found |
No such session, or it is not this account’s. Identical for both |
GET /gmb/connect-sessions
Section titled “GET /gmb/connect-sessions”Your recent sessions, newest first.
| Scope | read or gmb |
| Query param | Type | Default | Validation |
|---|---|---|---|
limit |
integer | 50 | 1–200; anything else is 400 invalid_input |
externalId |
string | — | Keep only the sessions made for this end user. The filter is part of the query, so limit counts this end user’s sessions |
curl "https://app.maukirim.com/api/v1/gmb/connect-sessions?externalId=client-4821&limit=20" \ -H "Authorization: Bearer mk_live_…"{ "ok": true, "sessions": [ { "id": "gms_7d2q9m4k1x", "state": "completed", "externalId": "client-4821", "connectionId": "gmc_5p1x8c3m7z", "error": null, "expiresAt": "2026-10-14T02:30:00.000Z", "createdAt": "2026-10-14T02:00:00.000Z", "completedAt": "2026-10-14T02:03:41.000Z" } ]}There is no paging cursor and no total. The list never contains a url.
Errors
Section titled “Errors”| HTTP | Code | Condition |
|---|---|---|
| 400 | invalid_input |
A bad limit |
| 402 | gmb_plan_required |
The package does not include the connector |
| 403 | api_key_scope_denied |
Neither read nor gmb |