Skip to main content

Order Completion Webhooks

Receive real-time notifications when esim_replacement or esim_delayed orders complete.

Overview

When you create an order with a callbackUrl, MobiMatter will send an HTTP POST to that URL when the order completes. This eliminates the need to poll the order status endpoint.

Supported product categories:

  • esim_replacement
  • esim_delayed

Other product categories silently ignore the callbackUrl parameter.

Configuration

Pass callbackUrl when creating an order:

POST /api/v2/order

{
"productId": "3HK_eSIM_replacement",
"addOnOrderIdentifier": "MM-123456",
"callbackUrl": "https://your-server.com/webhook/order-completed"
}

Webhook Payload

Your endpoint will receive a POST request with the following JSON body:

{
"eventType": "order.esim_replacement.completed",
"signature": "base64-encoded-rsa-sha256-signature",
"eventData": {
"orderId": "MM-123456",
"orderState": "Completed",
"merchantId": "your-merchant-id",
"currencyCode": "USD",
"created": "2026-03-25T10:30:00Z",
"updated": "2026-03-25T11:45:00Z",
"orderLineItem": {
"productId": "3HK_eSIM_replacement",
"productCategory": "esim_replacement",
"title": "3HK eSIM Replacement",
"provider": "15",
"providerName": "3HK",
"lineItemDetails": [
{ "name": "ICCID", "value": "89852..." },
{ "name": "SMDP_ADDRESS", "value": "rsp.example.com" },
{ "name": "ACTIVATION_CODE", "value": "K2-ABC123" },
{ "name": "LOCAL_PROFILE_ASSISTANT", "value": "LPA:1$..." },
{ "name": "QR_CODE", "value": "data:image/png;base64,..." }
]
}
}
}

The eventData field contains the same order structure you would get from GET /api/v2/order/{orderId}.

Verifying Signatures

Every webhook is signed with MobiMatter's RSA private key. The signature field contains a base64-encoded RSA-SHA256 signature. You verify it using MobiMatter's public key.

No shared secrets needed -- the public key is safe to distribute. Only MobiMatter can create valid signatures.

Signing String

The signature is computed over a deterministic string built from three fields in a fixed order:

{eventData.orderId}.{eventData.merchantId}.{eventData.orderLineItem.providerName}

For example, if orderId is MM-123456, merchantId is abc-def, and providerName is 3HK, the signing string is:

MM-123456.abc-def.3HK

MobiMatter Public Key

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr+ekCnv4b24XTBM0XEQW
MTEBFcV170q8kGZ21WSt9yLEV/2kTdu8u5gnDZ8lgtIExw0Yr16+YPFG3PborVjF
aGKxb6zEo/xJNEfIP8kLgdIf6KKdkUWGd4X1er10G1+VDt5k59lh7SHVv1R1Yb+2
RFaykihITukZ9Dpldf5nuap2sOCialer8HYrI+sCl/NxOpQP6EPxStXzqDR8eqVt
16WaX6CyTZHHqS/eRC9x20ehDc5ZJBNLIJjnOiObkeiPObgr5QJOt5j10TDfqW8o
wn2jK6G5rH8cks+LeebhVtisn9o5/aXfQ0P8wQT05M13dK670Dx4hKNSrDa6pBQi
oQIDAQAB
-----END PUBLIC KEY-----

Node.js

const crypto = require("crypto");

const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr+ekCnv4b24XTBM0XEQW
...
oQIDAQAB
-----END PUBLIC KEY-----`;

function verifyWebhook(payload) {
const { eventData, signature } = payload;
// Build the same signing string: orderId.merchantId.providerName
const signingString = `${eventData.orderId}.${eventData.merchantId}.${eventData.orderLineItem.providerName}`;
const verifier = crypto.createVerify("RSA-SHA256");
verifier.update(signingString);
return verifier.verify(PUBLIC_KEY, signature, "base64");
}

// In your handler:
const body = await request.text();
const payload = JSON.parse(body);
if (!verifyWebhook(payload)) {
return new Response("Unauthorized", { status: 401 });
}
// Process payload.eventData...

Python

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import base64

PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr+ekCnv4b24XTBM0XEQW
...
oQIDAQAB
-----END PUBLIC KEY-----"""

def verify_webhook(payload: dict) -> bool:
event_data = payload["eventData"]
signing_string = f"{event_data['orderId']}.{event_data['merchantId']}.{event_data['orderLineItem']['providerName']}"
signature = base64.b64decode(payload["signature"])
public_key = serialization.load_pem_public_key(PUBLIC_KEY_PEM.encode())
try:
public_key.verify(signature, signing_string.encode(), padding.PKCS1v15(), hashes.SHA256())
return True
except Exception:
return False

C#

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

bool VerifyWebhook(JsonElement payload, string publicKeyPem)
{
var eventData = payload.GetProperty("eventData");
var signingString = $"{eventData.GetProperty("orderId").GetString()}" +
$".{eventData.GetProperty("merchantId").GetString()}" +
$".{eventData.GetProperty("orderLineItem").GetProperty("providerName").GetString()}";
var signature = Convert.FromBase64String(
payload.GetProperty("signature").GetString());

using var rsa = RSA.Create();
rsa.ImportFromPem(publicKeyPem);
return rsa.VerifyData(
Encoding.UTF8.GetBytes(signingString),
signature,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
}

Retry Policy

If your endpoint returns a non-2xx status code, MobiMatter will retry delivery:

AttemptDelay
1Immediate
2~30 seconds
3~1 minute
4~5 minutes
5~15 minutes
6-8~30 minutes each

Total retry window: approximately 2 hours.

After all retries are exhausted, the webhook is marked as failed. You can check order status via GET /api/v2/order/{orderId} as a fallback.

Idempotency

Use the eventData.orderId as an idempotency key. Your endpoint may receive the same webhook more than once. Ensure you only process each order completion once.

Best Practices

  1. Respond quickly -- return 200 OK within 1 second Max. Process the order asynchronously if needed.
  2. Verify signatures -- always verify the RSA signature before processing.
  3. Handle duplicates -- use the orderId as an idempotency key.
  4. Return 200 on success -- any non-2xx response triggers a retry.
  5. Use HTTPS -- callback URLs must use HTTPS.