Skip to content

Developers

Webhooks and Zapier

Webhooks send an HTTP request to your server the moment something happens in your workspace, such as an app being paused or a smart link being clicked. Use them to update your own systems, post to Slack, or connect mapps.one to Zapier, Make or n8n without writing a server. Webhooks are available on the Studio plan, with up to 10 endpoints per workspace.

Add an endpoint

  1. In the dashboard, open Developers → Webhooks and choose Add endpoint. Workspace owners and admins can do this.
  2. Enter a public https URL, choose the events to send (or all events) and add a description.
  3. Copy the signing secret and store it with your server's other secrets.
  4. Choose Send test event to check that your endpoint responds.

The endpoint page shows recent deliveries with the response status, the first 2,000 characters of the response body and how long each attempt took. You can also create and delete endpoints with the API.

Events

TypeSent when
app.createdAn app was added to the workspace.
app.updatedApp details, links or page settings changed.
app.deletedAn app was deleted.
app.status_changedAn app went live, was paused or was suspended.
app.metadata_fetchedStore metadata was imported for an app.
legal.publishedA privacy policy or terms document was published.
domain.verifiedA custom domain was verified.
link.clickedEvery smart link click. This can be a lot of requests for a popular app.
analytics.daily_summaryThe previous day's click totals for each app, sent once a day.

Request format

Every event is sent as a POST request with a JSON body.

HeaderValue
Content-Typeapplication/json
User-Agentmapps.one-Webhooks/1.0
X-Mapps-EventThe event type, such as link.clicked
X-Mapps-DeliveryThe event ID. It stays the same on every retry.
X-Mapps-TimestampUnix time of this delivery attempt
X-Mapps-Signaturet={timestamp},v1={signature}

The body is an envelope with the same shape for every event:

{
  "id": "evt_01j8c3m9t5v2x7z4b6d8f0h2k4",
  "type": "app.status_changed",
  "created_at": "2026-09-14T09:41:07+00:00",
  "workspace": {
    "id": "01j8a0f4c7d2e9h6k3m5n8q1r4",
    "name": "Habitly Labs"
  },
  "data": {
    "app": {
      "id": "01j8b2k6r3xq9v7m4n5p0s1t2w",
      "slug": "habitly",
      "name": "Habitly"
    },
    "status": "paused"
  }
}

The contents of data depend on the event. Events about an app include an app object with its id, slug and name.

link.clicked

"data": {
  "app": { "id": "01j8b2k6r3xq9v7m4n5p0s1t2w", "slug": "habitly", "name": "Habitly" },
  "click": {
    "occurred_at": "2026-09-14 09:41:07",
    "os": "iOS",
    "device_type": "mobile",
    "country": "TR",
    "source": "instagram",
    "destination": "app_store",
    "utm_source": "instagram",
    "utm_medium": "social",
    "utm_campaign": "launch"
  }
}

occurred_at is in UTC. device_type is mobile, tablet or desktop. destination is a store key, web or landing. Fields that are unknown are null. Bot visits never trigger this event.

app.metadata_fetched

"data": {
  "app": { "id": "01j8b2k6r3xq9v7m4n5p0s1t2w", "slug": "habitly", "name": "Habitly" },
  "source": "app_store"
}

Verify signatures

Check the signature on every request, so you know it came from mapps.one and was not changed on the way:

  1. Read t and v1 from the X-Mapps-Signature header.
  2. Compute an HMAC-SHA256 of {t}.{raw request body} using your endpoint's signing secret, as lowercase hex.
  3. Compare it with v1 using a constant-time comparison.
  4. Reject the request if t is more than five minutes away from your server's clock, to block replayed requests.
Use the raw body. Sign the exact bytes you received, before any JSON parsing. Re-encoding the JSON can change slashes or Unicode characters and break the signature.

PHP

<?php

$secret = getenv('MAPPS_WEBHOOK_SECRET');
$body = file_get_contents('php://input');

parse_str(str_replace(',', '&', $_SERVER['HTTP_X_MAPPS_SIGNATURE'] ?? ''), $parts);

$timestamp = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $timestamp.'.'.$body, $secret);

if (! hash_equals($expected, (string) ($parts['v1'] ?? '')) || abs(time() - $timestamp) > 300) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);

if ($event['type'] === 'link.clicked') {
    // Store $event['data']['click'] ...
}

http_response_code(200);

In Laravel, read the body with $request->getContent() and the header with $request->header('X-Mapps-Signature'), and exclude the route from CSRF protection.

Node.js (Express)

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const secret = process.env.MAPPS_WEBHOOK_SECRET;

app.post('/webhooks/mapps', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('X-Mapps-Signature') ?? '';
  const parts = Object.fromEntries(header.split(',').map((pair) => pair.split('=')));
  const timestamp = Number(parts.t);
  const body = req.body.toString('utf8');

  const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${body}`).digest();
  const received = Buffer.from(parts.v1 ?? '', 'hex');
  const valid = received.length === expected.length && crypto.timingSafeEqual(received, expected);

  if (!valid || Math.abs(Date.now() / 1000 - timestamp) > 300) {
    return res.sendStatus(400);
  }

  const event = JSON.parse(body);

  if (event.type === 'link.clicked') {
    // Store event.data.click ...
  }

  res.sendStatus(200);
});

app.listen(3000);

Responses and retries

  • Respond with any 2xx status within 10 seconds. Do slow work in a background job after you respond.
  • Redirects are not followed, so use the final URL.
  • The URL must be public https. Private and local network addresses are refused.
  • If your endpoint times out or returns anything other than 2xx, we retry up to five more times: after about 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Each attempt has a new timestamp and signature.
  • The same event can arrive more than once, and events can arrive out of order. Use X-Mapps-Delivery, the event id, to ignore duplicates, and created_at to order them.
  • After 20 failed attempts in a row, the endpoint is turned off and nothing more is sent to it. Fix the problem, then turn it back on in the dashboard.

Zapier, Make and n8n

You can connect mapps.one to thousands of apps without running a server.

Zapier

  1. Create a Zap with Webhooks by Zapier as the trigger and choose Catch Hook.
  2. Copy the webhook URL Zapier gives you.
  3. In mapps.one, add an endpoint with that URL and pick the events you want, for example app.status_changed or analytics.daily_summary.
  4. Choose Send test event in mapps.one, then Test trigger in Zapier. The fields from the payload, such as data.app.name, can now be used in the next steps.

Examples: post a Slack message when an app is paused, add each day's clicks to a Google Sheet, or email a client when their domain is verified.

Make and n8n

In Make, use the Webhooks → Custom webhook trigger. In n8n, use the Webhook node with the POST method. Then add the URL as an endpoint in mapps.one as above.

Building an integration

If you are building a mapps.one integration for others, subscribe with the API when a user connects (POST /api/v1/webhooks), unsubscribe when they disconnect (DELETE /api/v1/webhooks/{id}), use GET /api/v1/events for the event picker, and use GET /api/v1/workspace to test the connection. See the API reference.