Skip to main content
View as Markdown

Webhooks

Webhooks tell your systems when something happens in your Mobile Next Cloud organization: a device becomes ready or is released, an uploaded app is verified, usage crosses a threshold. Mobile Next sends a signed POST with a JSON body to an HTTPS URL you choose, retries until your endpoint answers, and keeps a history of every delivery so you can inspect and resend it.

note

Webhooks are available to Team and Enterprise organizations. Personal workspaces and Hobby organizations can open the Webhooks page but cannot add webhooks.

This page covers setup, delivery and how to build a receiver. Two companion pages hold the details:

  • Verify signatures: code for Node, Python, Go, Ruby, Java and .NET, and test vectors.
  • Event types: every event, its data and an example body.

Quick start​

Only organization admins can add, change or test webhooks. Every member can see the list.

  1. In the web app, open Webhooks and choose Add webhook.

    The Webhooks page with two webhooks

  2. Enter your endpoint's URL, an optional description, and pick the events to send. Pick a group such as All allocation.* to also receive event types added to that group later.

    The Add webhook sheet, filled in

  3. Choose Add webhook. The signing secret is shown once: copy it now and store it where your receiver can read it, such as a secrets manager. Afterwards only its last four characters are shown.

    The Webhook created sheet showing the signing secret

  4. Open the webhook and choose Send test event. A webhook.test event is sent to this webhook only. Its row shows Succeeded once your endpoint answers with a 2xx status.

    A webhook's messages, with a test event that succeeded

  5. Choose a message to see its payload and every attempt: when it was made, the HTTP status your endpoint returned, how long it took, and the start of the response body. Resend delivers it again.

    A message with its attempts and payload

Your URL must use https, must not contain a username or password, and must resolve to a public address. Mobile Next does not call it when you add it; send a test event to check it.

Send events to Slack​

Paste a Slack incoming webhook URL (https://hooks.slack.com/services/...) as the webhook's URL. Slack only accepts its own message format, so Mobile Next sends each event to that URL as a Slack message instead of the event JSON:

  • the event type, such as app.uploaded, as the title;
  • the plain values of data (text, numbers, true/false), such as appName, version and url; nested objects, such as an allocation's device, are left out;
  • the event id and time.

Retries, the message history and Resend work as for any webhook. The history shows the event JSON, not the Slack message.

How it works​

Webhook. One endpoint: a URL, the events it receives, a signing secret and a status (enabled or disabled). A webhook receives the events of the whole organization. An organization can have up to 10 webhooks (25 on Enterprise), and two webhooks may share a URL.

Event. One thing that happened, such as allocation.released. It has an id, a type, a time and a data object. A webhook only receives events that happen while it is enabled; it never receives earlier ones.

Message. One event addressed to one webhook. Every retry and every resend of it is the same message.

Attempt. One HTTP POST of a message.

Event id and message id​

Each delivery carries two ids, and they answer different questions:

WhereSame across
Event idbody idevery webhook that receives the event, every retry, every resend
Message idwebhook-id headerevery retry and resend to this webhook; differs between webhooks

Deduplicate on the event id. If two of your webhooks point at the same receiver, they deliver the same event twice with two message ids but one event id.

The request​

POST /webhooks/mobilenext HTTP/1.1
Content-Type: application/json
User-Agent: MobileNext-Webhooks/1
webhook-id: 2f6b1c3a-8d4e-4b7f-9a1c-5e2d3f4a6b7c
webhook-timestamp: 1790000000
webhook-signature: v1,1xPyhRV/UPW2y1MaASMXIt9Qrrq4UpaskY7bYNvk2LE=

{"id":"5d2a8f3e-1b4c-4d6e-9f0a-2b3c4d5e6f7a","object":"event","type":"webhook.test","apiVersion":"2026-09-22","createdAt":"2026-09-21T14:13:20.117Z","data":{"webhook":"1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a"}}
  • webhook-id: the message id.
  • webhook-timestamp: when this attempt was sent, in Unix seconds. It is new on every attempt.
  • webhook-signature: v1, and a base64 HMAC-SHA256 of the request. While a secret is being rotated it holds two signatures separated by a space. See Verify signatures.

The body is the event:

Field
idEvent id.
objectAlways event.
typeThe event type. It decides the shape of data.
apiVersionThe payload version, a date: 2026-09-22. Breaking changes get a new date and are announced; new fields can appear at any time.
createdAtWhen the event happened.
dataDepends on type. See Event types.

The format is Standard Webhooks, so the standardwebhooks and svix libraries verify it without changes.

Delivery​

At least once, in any order​

A message is delivered at least once while its retries last. A network failure after your endpoint processed a request, a timeout, or a resend can each deliver it again. Deduplicate on the event id. After the 8th attempt fails the message is failed and is not retried on its own; watch for failed messages and resend them.

Messages are not ordered. A retry can arrive after a newer event, so an allocation.ready can arrive after the allocation.released of the same allocation. Use the event's state, not its arrival order: every allocation.* event carries data.allocationStatus, and the lifecycle rank tells you which one is newer.

Success and retries​

An attempt succeeds when your endpoint answers with any 2xx status within 10 seconds. Anything else is a failure: another status, a timeout, a connection or TLS error. Redirects are not followed.

A failed message is retried on this schedule, 8 attempts over about 27.5 hours:

AttemptSent
1Immediately
25 seconds after attempt 1
35 minutes after attempt 2
430 minutes after attempt 3
52 hours after attempt 4
65 hours after attempt 5
710 hours after attempt 6
810 hours after attempt 7

A message is pending until it succeeds (succeeded) or attempt 8 fails (failed). Deleting the webhook cancels its pending messages (canceled).

Resend​

Open the message and choose Resend, or call POST /api/v1/webhooks/{webhookId}/messages/{messageId}/resend. A resend of a succeeded or failed message makes exactly one new attempt now; the retry schedule does not start over. A resend of a pending message moves its next attempt to now.

After an outage on your side, pick a date range on the webhook's messages and choose Resend failed in range: every failed message in that range is resent once.

Messages, their payloads and attempts are kept for 30 days. A message can be resent until it is 29 days old.

Disabled webhooks​

You can disable a webhook from its menu. A disabled webhook gets no new messages; messages already pending keep retrying, so nothing queued is lost. Enable turns it back on. You can send a test event to a disabled webhook to check a fix first.

A webhook is disabled automatically when it has had no 2xx response for 72 hours and at least 5 of its messages failed in that time. Your organization's admins get an email when that happens. Fix the endpoint, send a test event, then enable it again.

Build a receiver​

Your endpoint has 10 seconds to answer. Don't do the work in the request; record the event and answer.

  1. Read the raw body. Verify the signature against the exact bytes you received, before parsing JSON. Many frameworks parse the body for you; turn that off for this route.
  2. Verify the signature and timestamp with your webhook's secret, as shown in Verify signatures. Answer 400 or 401 if it fails.
  3. Store it, deduplicated. Insert the event id and the raw body into a table with a unique key on the event id. If the insert fails because the key exists, you already have this event: answer 200 and stop.
  4. Answer 2xx once the row is committed.
  5. Process it asynchronously from that table, in a worker or queue. Make the work itself idempotent, or mark the row processed in the same transaction as the work.
  6. Keep the rows at least 30 days, the window in which a message can be resent.
CREATE TABLE webhook_inbox (
event_id VARCHAR(36) PRIMARY KEY, -- body "id"
type VARCHAR(64) NOT NULL,
body TEXT NOT NULL, -- the raw body, as received
received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP NULL
);

When in doubt about the current state of a resource, read it from the REST API.

Rotate the secret​

Choose Rotate secret from a webhook's menu, or call POST /api/v1/webhooks/{webhookId}/rotate-secret. The new secret is shown once.

  • Rotate keeps the current secret signing for 24 hours. Every request carries two signatures, one per secret, so your receiver keeps working with either. Deploy the new secret within that time. A second normal rotation is refused while the first one's 24 hours last.
  • Rotate immediately stops the current secret now. Use it if the secret leaked.

Manage webhooks with the API​

Everything in the web app is also in the REST API, with an API key. Reads work for every member; changes need the API key of an organization admin.

MethodPath
GET/api/v1/webhooks/event-typesThe event types you can subscribe to
POST/api/v1/webhooksAdd a webhook; the response has the secret, once
GET/api/v1/webhooksList webhooks
GET/api/v1/webhooks/{webhookId}Get one webhook
PATCH/api/v1/webhooks/{webhookId}Change url, description, events or status
DELETE/api/v1/webhooks/{webhookId}Delete a webhook and its history
POST/api/v1/webhooks/{webhookId}/rotate-secretNew secret; expireCurrentIn seconds of overlap, 0 to 86400 (default)
POST/api/v1/webhooks/{webhookId}/testSend a webhook.test event; returns the message
GET/api/v1/webhooks/{webhookId}/messagesMessages, newest first; filter by status, type, createdAfter, createdBefore
GET/api/v1/webhooks/{webhookId}/messages/{messageId}One message; admins also get its payload and attempts
POST/api/v1/webhooks/{webhookId}/messages/{messageId}/resendDeliver it again now
curl -s https://app.mobilenext.ai/api/v1/webhooks \
-H "Authorization: Bearer $MOBILENEXT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"url": "https://example.com/webhooks/mobilenext", "description": "CI notifier",
"events": ["allocation.*", "usage.*"]}'
{
"id": "6f1c8e4a-1f2b-4a1e-9a2c-0b7f5d3e6a11",
"object": "webhook",
"url": "https://example.com/webhooks/mobilenext",
"description": "CI notifier",
"events": ["allocation.*", "usage.*"],
"status": "enabled",
"disabledReason": null,
"secretSuffix": "x7Qk",
"organization": "0b7f5d3e-6a11-4a1e-9a2c-6f1c8e4a1f2b",
"createdAt": "2026-09-22T10:00:00Z",
"updatedAt": "2026-09-22T10:00:00Z",
"secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
}

events takes exact event types, a group such as allocation.*, or ["*"] for everything. Sending a test event is limited to 10 per webhook per minute, and resending to 60.

FAQ​

Why did I get the same event twice?​

Delivery is at least once. The usual causes: your endpoint took longer than 10 seconds or the connection dropped, so the attempt counted as failed and was retried even though you had processed it; someone resent the message; or two of your webhooks receive the same event. Deduplicate on the body id, which is the same in all these cases.

Why did events arrive out of order?​

Messages are retried independently, so a retried older event can arrive after a newer one. Don't rely on arrival order or on createdAt alone. For an allocation's current state, keep the highest lifecycle rank you have seen per allocation; a lower-ranked event doesn't move it back, but still tells you what happened, such as when the device became ready.

Why was my endpoint disabled?​

It had no 2xx response for 72 hours and at least 5 messages failed in that time. Open the webhook, filter messages by Failed, and open one to see what your endpoint answered on each attempt. Fix it, send a test event, then choose Enable. Resend the failed messages you still need; they are not resent automatically.

Why did signature verification fail?​

The most common causes: the body was parsed and re-serialized before verifying, the secret is from another webhook or was rotated, the whsec_ prefix was not removed before base64 decoding, or your server's clock is more than 5 minutes off. See Verify signatures.

Can I receive webhooks on localhost?​

No. The URL must resolve to a public address. Use a tunnel that gives you a public HTTPS URL while developing.