Troubleshooting
This page covers the most common issues developers run into when working with PayMongo webhooks, error handling, and the MCP server — and how to resolve them. If you're seeing unexpected behavior, start here before opening a support ticket.
Webhooks
Events aren't arriving at my endpoint
Work through these checks in order.
- Confirm your endpoint is registered. Go to Developers → Webhooks in the Dashboard and verify your endpoint URL is listed and enabled.
- Check the endpoint status. A disabled endpoint won't receive any events. Re-enable it from the Dashboard or via
POST /v1/webhooks/{id}/enable. - Verify your endpoint is publicly accessible. PayMongo cannot deliver to localhost or private network addresses. If you're developing locally, use a tunneling tool like ngrok to expose your local server.
- Confirm your endpoint returns a
2xx. If your handler is throwing an error before it responds, PayMongo may be retrying silently. Check your server logs for unhandled exceptions. - Check your event subscriptions. Your endpoint may not be subscribed to the event type you're expecting. Review the subscribed events for your endpoint in the Dashboard.
Events are arriving but being retried repeatedly
This means PayMongo is not receiving a 2xx response from your handler.
- Check for unhandled exceptions. An uncaught error in your handler will likely produce a 5xx response. Add a top-level error handler that always returns
200for requests that pass signature verification, even when processing fails — log the failure and handle it asynchronously. - Check your response time. If your handler takes longer than 30 seconds to respond, PayMongo treats it as a timeout and retries. Move any slow operations to a background job and return
200immediately. - Check for signature verification failures. If your handler returns a
401on failed verification, legitimate events won't cause retries — but misconfigured verification can cause valid events to be rejected.
Signature verification fails on valid requests
Signature verification failures on otherwise valid requests are almost always caused by one of the following.
- The request body is being parsed before verification. JSON parsing middleware modifies the raw bytes, which breaks the HMAC check. Configure your framework to pass the raw body to your verification step before any parsing occurs.
// Express — use raw body parser for the webhook route only
app.post(
"/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
// req.body is the raw Buffer here — safe to verify
}
);- The wrong secret is being used. Each webhook endpoint has its own signing secret — it's not the same as your API secret key. Confirm you're using the secret from Developers → Webhooks → [your endpoint] → Signing secret in the Dashboard.
- The secret has been rotated but not updated in your environment. If you recently rotated your webhook secret, make sure the new value is deployed to your server. The old secret is invalidated immediately on rotation.
- Whitespace or encoding issues. If your secret is stored in an environment variable, check for accidental leading or trailing whitespace introduced during copy-paste.
Duplicate events are triggering duplicate processing
This is expected behavior — retries mean the same event may arrive more than once. Your handler should be idempotent. Use the event id field to deduplicate.
const eventId = payload.data.id;
if (await db.processedEvents.exists(eventId)) {
return res.sendStatus(200); // Already handled — acknowledge and skip
}
await db.processedEvents.insert(eventId);
// Continue processingError handling
I'm getting a 401 Unauthorized response
Authentication errors mean PayMongo cannot validate your API key. Check the following.
- Confirm you're using the right key for the environment. Test keys (pk_test_ / sk_test_) only work against test resources, and live keys (
pk_live_/sk_live_) only work against live resources. Mixing them returns a401. - Check how you're sending the key. PayMongo uses HTTP Basic Auth — your API key goes in the username field with an empty password. A common mistake is sending the key as a Bearer token instead.
Authorization: `Basic ${btoa(apiKey + ":")}`Authorization: `Bearer ${apiKey}`- Check for extra whitespace. If your key is read from an environment variable, confirm there's no leading or trailing whitespace.
- Confirm the key hasn't been revoked. Check Settings → API Keys in the Dashboard to verify the key is still active.
I'm getting a 422 Unprocessable Entity response
A 422 means your request was received but failed validation. Check the errors array in the response body — the source.pointer field identifies exactly which parameter is invalid.
{
"errors": [
{
"code": "parameter_invalid",
"detail": "amount must be at least 2000.",
"source": {
"pointer": "attributes.amount"
}
}
]
}Common causes include amounts below the minimum allowed value, missing required fields, and invalid currency codes. Fix the flagged parameter and retry — 422 errors are never retryable as-is.
I'm only handling the first error but there are multiple
PayMongo can return more than one error object in the errors array — for example, when multiple required fields are missing in the same request. Always iterate the full array.
const { errors } = await response.json();
for (const error of errors) {
console.error(`[${error.code}] ${error.detail}`);
}Error detail messages are showing up in my UI
detail messages are showing up in my UIThe detail field is intended for server logs, not end users. It may contain field names, parameter values, or internal descriptions that shouldn't be customer-facing. Map error code values to your own user-friendly messages instead.
const userMessages = {
card_declined: "Your card was declined. Please try a different payment method.",
insufficient_funds: "Your card has insufficient funds. Please try a different card.",
parameter_invalid: "Something went wrong with your request. Please try again."
};
const message = userMessages[error.code] ?? "An unexpected error occurred. Please try again.";Updated about 3 hours ago