Skip to content

Webhooks

MoreLogin can POST a JSON event to an HTTPS endpoint you own as soon as a cloud phone or cloud browser operation reaches its final result, so you no longer have to poll for the outcome. One callback URL is stored per team, and the Local API and Open API write to the same record.

Configure the callback URL

Both surfaces expose the same operation. Open API:

curl -X POST "https://api.morelogin.com/webhook/config" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "callbackUrl": "https://example.com/morelogin/webhook",
    "enabled": true
  }'

Local API:

curl -X POST "http://127.0.0.1:40000/api/webhook/config" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://example.com/morelogin/webhook",
    "enabled": true
  }'

The Local API route is served by the desktop client, so it needs MoreLogin client v2.66.0 or higher. The Open API endpoint has no client dependency.

teamId is never read from the request. Each call configures the team of the server-side context, so you cannot point another team's events at your endpoint.

The URL must be HTTPS, 1 to 1024 characters, with a host, no user info, no fragment and a valid port. Every address it resolves to has to be publicly routable: localhost, private, link-local, CGNAT, cloud metadata, protocol-reserved, documentation and 6to4 ranges are rejected with 41001. The same check runs again when a delivery opens its connection, so a hostname that later resolves into a private range fails that delivery rather than reaching your network.

Saving is an upsert keyed by team, so sending the same body twice is harmless.

Turning delivery off

enabled: false keeps the URL on file and stops new events from being created. Events already queued are cancelled one at a time, at the moment each would next be authorized, and only while the configuration is still disabled — so if you re-enable quickly, queued events continue instead of being dropped.

What a delivery looks like

Each delivery is a POST with these headers:

HeaderValue
Content-Typeapplication/json
User-AgentMoreLogin-Webhook/1.0
X-Webhook-Event-IdSame as eventId in the body
X-Webhook-Event-TypeSame as eventType in the body

The two X-Webhook-* headers duplicate body fields on purpose, so a router or queue can dispatch on them without parsing JSON.

{
  "eventId": "c7f3a2b10d8e4f6a9c1b2d3e4f5a6b7c",
  "eventType": "cloud_phone.power_off",
  "eventTime": "1787241600000",
  "data": {
    "teamId": "10001",
    "cloudPhoneId": "20001",
    "reason": "MONEY_SAVING"
  },
  "result": {
    "success": true,
    "code": "0",
    "message": null
  }
}
FieldDescription
eventIdUnique event ID. Retries and internal replays of the same event reuse it, so it is the idempotency key
eventTypeStable event name, from the table below
eventTimeWhen the final result happened, as Unix epoch milliseconds. Sent as a string
data.teamIdTeam that owns the resource. Sent as a string
data.cloudPhoneIdCloud phone the event is about. Sent as a string
data.cloudBrowserIdCloud browser profile the event is about. Sent as a string
data.runIdCloud browser run instance the event is about
data.reasonOnly on cloud_phone.power_off and cloud_browser.stop
result.successWhether the operation ultimately succeeded
result.code0 on success; otherwise a stable business error code that does not depend on language
result.messagenull on success; otherwise an explanation in the team's language, falling back to en-US

Every numeric identifier and eventTime arrive as JSON strings, matching the rest of the API — see Common Response Format. Parse them as strings, not as numbers, or you will lose precision on 64-bit IDs.

Events

eventTypeFinal result it reports
cloud_phone.power_onPower on succeeded or failed
cloud_phone.power_offPower off succeeded. A failed shutdown produces no event
cloud_phone.restartRestart succeeded or failed
cloud_phone.resetReset succeeded or failed
cloud_phone.new_machineOne-click new machine succeeded or failed
cloud_browser.startCloud browser start succeeded or failed
cloud_browser.stopEach cloud browser stop attempt, successful or not

Cloud phone events carry teamId and cloudPhoneId. Cloud browser events carry teamId, cloudBrowserId and runId.

Power on, restart, reset and one-click new machine report success: true only once the provider callback succeeded and the local synchronisation committed. A billing failure, or an asynchronous proxy check that fails and finishes its cleanup, reports a failed power on and does not additionally emit a power off. Deferred work such as installing preset apps or clearing expired files does not change the result.

If a shutdown interrupts a boot, that attempt produces no power-on event; the later successful shutdown still produces its own cloud_phone.power_off.

Stop reasons

cloud_phone.power_off adds data.reason:

ValueMeaning
NORMALOrdinary shutdown
MONEY_SAVINGMoney-saving mode
OTHERArrears, expiry, maintenance or anything else

cloud_browser.stop adds data.reason:

ValueMeaning
NORMALOrdinary close
MONEY_SAVINGMoney-saving mode
ENERGY_SAVINGEnergy-saving mode
BROWSER_EXITEDThe browser exited unexpectedly
OTHERAnything else

An internal archiving failure changes only result; it never overwrites the reason captured when the stop began.

Delivery, retries and ordering

Any 2xx counts as delivered. Every other status, plus connect and read timeouts and network errors, counts as a failure. The connect timeout is 5 seconds and the read timeout is 30 seconds.

A failed delivery is retried at most 6 times, after 10 seconds, 30 seconds, 60 seconds, 3 minutes, 10 minutes and 30 minutes. That is at most 7 requests for one event. After the sixth retry fails the event stops being retried automatically.

Events for the same cloud phone, or the same cloud browser, are delivered in creation order, one at a time. Different resources are delivered in parallel, so do not assume any ordering between them.

Retries of one event always reuse the same eventId, eventTime and body. Under network or recovery edge cases the same event may be delivered more than once with those values unchanged, so deduplicate on eventId.

For cloud browser stops, one stop attempt yields exactly one event. A later manual retry, a manual forced release, or a system-initiated forced release is a new operation and gets a new eventId. Two attempts are never merged into one event.

Cloud phone operations are identified by the provider task ID, traceId, or a stable business timestamp. When none of those exists the service generates a UUID and treats the current final callback as a new operation, so you get a new eventId rather than two different operations collapsing into one.

Writing the receiver

Return a 2xx as soon as you have stored the event, and do the real work asynchronously. The response body is ignored, and a slow handler burns your 30-second read budget and pushes the event into the retry schedule even though you received it.

Deduplicate on eventId. Treat a repeat as already handled rather than replaying side effects.

Do not treat the payload as trusted input just because it arrived at your endpoint. This release does not sign callbacks, so anyone who learns your URL can post to it. Use a URL that is hard to guess, and before acting on anything expensive, confirm the state through the API — for example POST /cloudphone/info for a cloud phone, or POST /cloudbrowser/page for a cloud browser run.

result.code uses the same business codes as the synchronous API, so a failure can be looked up in Error Codes. result.message is a human-readable string whose wording and language can change; branch on result.success and result.code, never on result.message.

The machine-readable contract for every event, including schemas and per-event examples, is published with the shared resources spec under Shared Resources Open API.