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_replacementesim_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:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 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
- Respond quickly -- return
200 OKwithin 1 second Max. Process the order asynchronously if needed. - Verify signatures -- always verify the RSA signature before processing.
- Handle duplicates -- use the
orderIdas an idempotency key. - Return 200 on success -- any non-2xx response triggers a retry.
- Use HTTPS -- callback URLs must use HTTPS.