Skip to content

Developer docs

Munja runs on the page customers see after they pay or book. It never touches checkout.

Sign in to see samples with your own test keys.

Quick start

Add a container where offers should appear, load the script with your publishable key, and queue a show call. The queue works before the script has loaded.

The customer's name and email stay in the browser until they say yes to an offer. If there is nothing worth showing, the container stays empty and the page looks as it did.

Your keys are in Install in your dashboard. Use pk_test_ while building.

Confirmation page (HTML)
<div id="munja-moment"></div>
<script async src="https://getmunja.com/v1.js" data-key="pk_test_..."></script>
<script>
  window.Munja = window.Munja || [];
  Munja.push(["show", {
    order: { id: "ORDER-1042", signature: "SIGNED_ON_YOUR_SERVER" },
    customer: { email: "dana@example.com", name: "Dana" },
    context: { zip: "60614", newCustomer: true, orderTotal: 129.0 }
  }]);
</script>

Signed orders

Sign each order on your server so fake traffic cannot earn from your account. The signature is HMAC-SHA256, in hex, of order_id + "." + email, with the email trimmed and lowercased, using your signing secret (mss_test_…).

A wrong signature is always rejected. A missing one is accepted unless you turn on require signed orders in your settings. Never put the signing secret in a browser.

Sign the order on your server · Node
import { createHmac } from "node:crypto";

// orderId and email are the same values you pass to Munja.show
const signature = createHmac("sha256", process.env.MUNJA_SIGNING_SECRET)
  .update(`${orderId}.${email.trim().toLowerCase()}`)
  .digest("hex");

Order context

Context decides which offers are relevant. Every field is optional. Send what you know.

  • zip (or any key ending in Zip, such as toZip): local partners only appear in the areas they serve.
  • newCustomer: true for a first order. Offers can target new or returning customers.
  • orderTotal in dollars, or orderTotalCents in cents: offers can set a minimum or maximum order value.
  • products (also items, skus, lineItems): names or SKUs, or objects with name, title or sku.
  • Dates end in Date and use YYYY-MM-DD.
Fields by industry
IndustryFields
MovingmoveDate (YYYY-MM-DD), homeSize (Studio | 1 bedroom | 2 bedrooms | 3 bedrooms | 4+ bedrooms), fromZip (5 digits), toZip (5 digits), distance (Local | Long distance)
Pet suppliespetName, species (Dog | Cat), breed, age, firstOrder (Yes | No), zip (5 digits)
Furnitureitem (Sofa | Bed frame | Dining table | Desk), deliveryDays, replacing (Replacing an old one | Furnishing a new space), zip (5 digits)
Electronicsproduct (TV | Laptop | Phone | Smart home kit | Gaming console), price, zip (5 digits)
Landscapingservice (Weekly mowing | Spring cleanup | Fall cleanup | Tree trimming | Patio install), lot (Small | Medium | Large), irrigation (Yes | No), zip (5 digits)
Heating and coolingservice (Furnace install | AC repair | Tune-up | Duct cleaning), systemAge, homeAge, zip (5 digits)
Auto repairservice (Oil change | Brake repair | Tire replacement | Major repair), mileage, vehicleAge, zip (5 digits)
Fashion and beautyitem (Clothing | Shoes | Accessories | Skincare | Makeup | Fragrance), price, zip (5 digits)
Home and kitchenitem (Mattress | Appliance | Cookware | Bedding | Decor | Lighting), price, zip (5 digits)
Baby and kidsitem (Stroller | Car seat | Crib | Nursery furniture | Clothing | Toys), price, zip (5 digits)
Sports and outdoorsitem (Bike | Camping gear | Fitness equipment | Golf | Ski and snowboard | Running), price, zip (5 digits)
Food and drinkitem (Coffee | Meal kit | Wine | Snacks | Specialty food), price, zip (5 digits)
Other e-commerceitem, price, zip (5 digits)
Plumbingservice (Water heater install | Drain cleaning | Leak repair | Repipe | Sewer line), when (YYYY-MM-DD), zip (5 digits)
Roofing and exteriorsservice (Roof replacement | Roof repair | Gutters | Siding | Windows), when (YYYY-MM-DD), zip (5 digits)
Remodelingservice (Kitchen remodel | Bathroom remodel | Basement finish | Flooring | Painting), when (YYYY-MM-DD), zip (5 digits)
Junk removal and haulingservice (Junk removal | Furniture removal | Appliance removal | Full cleanout | Construction debris), when (YYYY-MM-DD), zip (5 digits)
Cleaningservice (Recurring cleaning | Deep clean | Move-out clean | Carpet cleaning | Window cleaning), when (YYYY-MM-DD), zip (5 digits)
Pest controlservice (General pest | Termites | Rodents | Mosquitoes | Bed bugs), zip (5 digits)
Solar and electricalservice (Solar install | Panel upgrade | EV charger install | Generator | Rewiring), when (YYYY-MM-DD), zip (5 digits)
Other home servicesservice, price, zip (5 digits)
Car salesvehicle (Used car | New car | Lease | Truck | EV), when (YYYY-MM-DD), price, zip (5 digits)
Collision and glassservice (Collision repair | Dent repair | Windshield replacement | Paint), zip (5 digits)
Car wash and detailingservice (Full detail | Ceramic coating | Wash membership | Window tint), zip (5 digits)
Other auto servicesservice, price, zip (5 digits)
Hotels and rentalsstay (Hotel stay | Vacation rental | Resort | Cabin), when (YYYY-MM-DD), price, zip (5 digits)
Tours and activitiesitem (City tour | Boat tour | Day trip | Theme park | Museum pass), when (YYYY-MM-DD), zip (5 digits)
Other travel and hospitalityservice, price, zip (5 digits)
Gyms and studiositem (Gym membership | Class pack | Personal training | Yoga | Pilates), zip (5 digits)
Salons and spasservice (Haircut | Color | Massage | Facial | Nails), when (YYYY-MM-DD), zip (5 digits)
Other health and wellnessservice, price, zip (5 digits)
Events and ticketsitem (Concert | Sports game | Theater | Festival | Conference), when (YYYY-MM-DD), zip (5 digits)
Weddings and partiesservice (Venue | Photography | Catering | Florist | Rentals), when (YYYY-MM-DD), zip (5 digits)
Other events and weddingsservice, price, zip (5 digits)
Property managementunit (Studio | 1 bedroom | 2 bedrooms | 3+ bedrooms | Single-family home), when (YYYY-MM-DD), zip (5 digits)
Estate sales and downsizingservice (Estate sale | Downsizing | Online auction | Cleanout | Appraisal), when (YYYY-MM-DD), zip (5 digits)
Home salesdeal (Home purchase | Home sale), when (YYYY-MM-DD), zip (5 digits)
Self storageitem (Small unit | Medium unit | Large unit | Climate controlled), when (YYYY-MM-DD), zip (5 digits)
Other real estateservice, price, zip (5 digits)
Tax and accountingservice (Tax return | Bookkeeping | Business formation | Payroll setup), zip (5 digits)
Legal servicesservice (Estate planning | Business contracts | Real estate closing | Trademark), zip (5 digits)
Other professional servicesservice, price, zip (5 digits)
Classes and coursesitem (Online course | Tutoring | Coding bootcamp | Music lessons | Driving school), when (YYYY-MM-DD), price, zip (5 digits)
Other educationitem, price, zip (5 digits)
Order context
context: {
  zip: "60614",              // decides which local partners can appear
  newCustomer: true,         // first order with you (true, false, or leave out)
  orderTotal: 129.00,        // dollars; or orderTotalCents: 12900
  products: [                // names or SKUs, or { name, sku } objects
    { name: "2 bedroom move", sku: "MOVE-2BR" }
  ],
  moveDate: "2026-11-14"     // industry fields: see the table
}

Exit popup

Add data-exit="on" to the script on every page to show offers when a visitor is about to leave. Turn it on and set how often it may appear in your dashboard.

It shows at most once a visit and no more often than the frequency you set. It stays out while a known popup tool (or a selector you add) is open, and for the rest of the visit after you call sitePopupShown. Leave the attribute off your checkout and cart pages.

Every page (HTML)
<script async src="https://getmunja.com/v1.js" data-key="pk_test_..." data-exit="on"></script>

<!-- If your site opens its own popup, tell Munja so it stays out for the visit -->
<script>
  window.Munja = window.Munja || [];
  Munja.push(["sitePopupShown"]);
</script>

Shopify, WordPress and others

The Shopify app and the WordPress plugin are not published yet. Until they are, paste the snippet into the page customers see after paying: on Shopify, the order status page's additional scripts; on WooCommerce, the order-received page using a code snippets plugin.

For Google Tag Manager, use a Custom HTML tag that fires on your thank-you page and maps data layer values into order and context.

If you render the page on your server, you can put the order on the script tag instead of calling show.

Any server-rendered confirmation page
<!-- Fill these from your own order record when the page renders -->
<div id="munja-moment"></div>
<script async src="https://getmunja.com/v1.js"
        data-key="pk_test_..."
        data-order="ORDER_ID"
        data-signature="SIGNATURE_FROM_YOUR_SERVER"
        data-email="CUSTOMER_EMAIL"
        data-name="CUSTOMER_FIRST_NAME"></script>

Get offers

The embed calls this endpoint. Call it directly for receipt emails, customer portals or native apps. JSON in and out, snake_case keys, money as integer cents. It takes your publishable key in the body and allows 600 requests a minute per IP.

surface is one of thank_you, exit, email or portal. An empty data list means show nothing; empty_reason says why.

POST /v1/offers · Node
const res = await fetch("https://getmunja.com/v1/offers", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    key: "pk_test_...",
    order: { id: "ORDER-1042", signature },
    customer: { email: "dana@example.com", name: "Dana" },
    context: { zip: "60614" },
  }),
});
const { data } = await res.json(); // [] means show nothing
Response
{
  "object": "offer_list",
  "decision": "dec_...",
  "livemode": false,
  "data": [
    {
      "impression": "imp_token_...",
      "position": 1,
      "partner": "Partner name",
      "headline": "...",
      "body": "...",
      "cta": "Yes, contact me",
      "consent": "Text shown next to the button"
    }
  ],
  "moment": { "merchant": "...", "title": "...", "layout": "stack" }
}

Accept an offer

When the customer says yes, send the impression token with their email and name. The partner is told about the lead and the customer sees the confirmation message. There is also a matching /dismiss endpoint.

POST /v1/offers/{impression}/accept · Node
await fetch(`https://getmunja.com/v1/offers/${encodeURIComponent(offer.impression)}/accept`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "dana@example.com", name: "Dana" }),
});

Send leads from your server

If you show offers in your own interface, send each acceptance with your secret key (sk_test_…, server only) and the exact disclosure text the customer saw. Every POST needs an Idempotency-Key header, so retries never create a second lead.

A lead from the same email for the same partner within the duplicate window comes back with status: "duplicate" and is not charged.

POST /api/v1/leads · Node
const res = await fetch("https://getmunja.com/api/v1/leads", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MUNJA_SECRET_KEY}`,
    "Idempotency-Key": `${orderId}-${offerId}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ offer_id: offerId, email, disclosure_text: shownText, contact: { name } }),
});
// 201 { "id": "lead_...", "object": "lead", "status": "charged" | "duplicate", ... }

Partner: conversions and refunds

Partners report sales with the mj_click_id from the lead (or the lead ID), so merchants can see what their customers bought. Value is in cents.

Request a refund for a bad lead with a reason: duplicate, wrong_contact (invalid contact details), out_of_area, test, fraud or other, within 14 days of the lead.

POST /api/v1/conversions (partner key) · Node
await fetch("https://getmunja.com/api/v1/conversions", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.MUNJA_SECRET_KEY}`, "Idempotency-Key": externalId, "Content-Type": "application/json" },
  body: JSON.stringify({ mj_click_id: clickId, type: "purchase", value: 4900, external_id: externalId }),
});
POST /api/v1/refund_requests (partner key) · curl
curl https://getmunja.com/api/v1/refund_requests \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: refund-lead_..." \
  -H "Content-Type: application/json" \
  -d '{"lead":"lead_...","reason":"wrong_contact","note":"Number disconnected"}'

Lead webhooks

Add an endpoint in your dashboard to receive lead.created and other events. Each delivery is signed with your endpoint secret in the X-Munja-Signature header: t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">. Reject anything older than 5 minutes.

Answer with any 2xx. Failed deliveries retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, and can be replayed from the delivery log. An endpoint that keeps failing for 3 days is turned off. More in webhook signatures.

lead.created
{
  "id": "evt_...",
  "object": "event",
  "type": "lead.created",
  "livemode": false,
  "created": 1793548800,
  "data": {
    "object": {
      "object": "lead",
      "id": "lead_...",
      "mj_click_id": "mjc_...",
      "offer": "ofr_...",
      "merchant": "acct_...",
      "price_cents": 3800,
      "created": 1793548800,
      "customer": { "email": "dana@example.com", "name": "Dana" }
    }
  }
}
Verify X-Munja-Signature · Node
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody is the exact request body string, before JSON parsing
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Errors

Errors return { error: { type, code, message, param, doc_url } } with a matching HTTP status. See the API reference for every code.

Error
HTTP/1.1 401
{
  "error": {
    "type": "authentication_error",
    "code": "invalid_signature",
    "message": "The order signature does not match. ...",
    "param": "signature",
    "doc_url": "/docs/api#invalid_signature"
  }
}

Test mode

Use pk_test_ and sk_test_ keys while building. Offers show a small test notice, acceptances create test leads, and no one is charged or paid.