maesn
Product insight

Unified error handling turns every failure into one predictable shape

Raw APIs disagree about where an error lives, what it is called and whether it is worth retrying. Maesn returns one envelope with a standardised category, catches invalid input before it is sent and passes the target system's own error through intact.

Three error dialectsone envelope
System A{ "Message": "…" }
System B{ "fault": { "code": 12 } }
System CHTTP 200, error in body
maesn categorises it
Always this shape
"errors": {
  "type": "target_system_error",
  "statusCode": 400,
  "downstreamErrors": [{
    "statusCode": 500,
    "message": "Unknown currency"
  }]
}
The system answered 500 for a currency code that does not exist. It will fail the same way every time, so the envelope carries the 400 the case deserves and the system's own answer travels with it.
Trusted by winning software teams
HubSpotTipaltiPaywiseRallyQredNordhealthFindityFintoClockinHEROHolviLanes & PlanesAgicapHubSpotTipaltiPaywiseRallyQredNordhealthFindityFintoClockinHEROHolviLanes & PlanesAgicap
The concept

What is unified error handling

Integrations are judged on how they behave when something goes wrong, not when everything works. The happy path is the easy half: you can build it against one system in an afternoon. The other half is knowing, at three in the morning, whether a failed sync means a malformed payload, a customer who revoked access, an endpoint that never existed for that system, or a provider having a bad hour, and which of those is worth trying again.

Unified error handling is the layer that makes that answerable. One envelope, one standardised set of categories and one documented retry policy, applied across every system, so your failure handling is written once. It leans on the same normalisation as the common data model: because Maesn knows what a valid object looks like, it can reject a bad one before any system sees it.

The problem

Every system fails in its own dialect

Failure is the least standardised behaviour of any API, and the one you meet under the most pressure. Four questions, four different answers per system.

Where the error lives
status codein the bodyin a fault
What it is called
messageMessagefault.code
Is it your fault
unclear400 for bothno category
Should you retry
undocumentedguesstry again
How Maesn handles it

One envelope, six useful fields

Errors arrive in a single top-level object with the same fields every time. Two of them are the reason this is more than cosmetic normalisation.

Every error, one envelope
typeThe standardised category, so you can branch on the kind of failure instead of parsing prose.
statusCodeThe standard HTTP code, matching the category, so the two never contradict each other.
messageA short explanation of what went wrong, meant for a human reading a log, not for parsing.
contextWhich target system, which Unified API and the timestamp, so you can locate the request.
validationErrorsWhat Maesn rejected before sending, field by field, when your input did not meet the schema.
downstreamErrorsWhat the target system itself reported, passed through verbatim.

A category, not a sentence

Every error carries a standardised type, so your code branches on the kind of failure instead of matching strings.

Caught before it is sent

Maesn checks field formats, required values and data types first, so invalid input fails fast with the field named.

Downstream errors kept

When a target system rejects a request, its original message and status come through untouched instead of being summarised away.

Retry guidance per code

Which responses are worth retrying, which are not and how long to wait is documented rather than left to trial and error.

error.json
"errors": {
"type": "invalid_parameters",
"statusCode": 400,
"message": "Invalid parameters, please check
the validationErrors array.",
"context": {
"targetSystem": "snelstart",
"unifiedApi": "Accounting"
},
"validationErrors": [
"Contact type must be COMPANY."
]
}

The two that carry the weight are statusCode and downstreamErrors, and one is normalised precisely so the other does not have to be. HTTP is specific about which code belongs to which situation, and systems implement that as loosely as they implement everything else: a validation failure, which no amount of retrying will fix, comes back from some systems as a 500. A 500 says the opposite, that something broke on their side and the same request may well succeed in ten minutes. Maesn answers with the code the situation actually calls for, so your retry logic reads a category instead of a mistake.

Correcting the code would normally cost you what sits underneath it, and that part is often the only useful one: which field was rejected, what the system expected instead, or a status of its own that means something only there. So the original is not replaced. It travels alongside the corrected one in downstreamErrors, which is why your application can branch on the category while a developer reads the system's own words, rather than the two being traded against each other.

Not every problem answers with an error. A call can succeed and still not do everything you asked, and that arrives as meta.warnings on the response: a field the target system does not use, or a value it ignores because it does not support it, such as a currency outside the ones it handles. It is written for the developers integrating rather than for the end user, and it is what separates a write that worked from a write that worked the way you meant.

Error types

Ten types, five decisions

The type field is the part worth building against. Ten standardised values cover every failure the Unified API produces, and each one implies a different response: fix and resend, ask the customer to reconnect, stop trying because the endpoint does not exist for that system, or back off and try later. Grouping them that way turns error handling from a pile of special cases into four or five branches you write once.

What it deliberately does not do is flatten the detail. A rate limit from a target system arrives as its own type rather than being folded into a generic failure, because the right reaction differs from a server error, and because the limit belongs to the target system and is yours to respect. Pacing runs on the asynchronous endpoints, so on a synchronous call the type is the whole signal. An expired connection arrives as its own type too, because the fix is not a retry but a customer walking back through authentication.

Your request

Fix it and resend

invalid_parametersconflict

Authorization

The customer reconnects

not_authorizedinactive_usertarget_system_credentials_error

Not available

Do not retry, it will not appear

resource_not_supportedtarget_system_not_found

The target system

Read the downstream error

target_system_errortarget_system_rate_limit_error

Temporary

Back off and retry

service_unavailable
Retries and outages

What to try again, and what never to

Retrying the wrong error wastes traffic and hides a real bug. Retrying the right one is often the difference between a failed sync and a slow one.

429Rate limited by the systemRetryWait a minute, then double the delay each time
500Internal server errorRetryWait 15 to 30 seconds, then back off exponentially
503Target system unavailableRetryWait 15 to 30 seconds, then back off exponentially
400Payload failed validationNo retryFix the fields in validationErrors and resend
401Credentials no longer validNo retryHave the customer reconnect the integration
409The resource already existsNo retryCheck the request body before sending again

Two layers are at work here and it is worth keeping them apart, starting with the fact that the table above is written for your code rather than describing ours. Maesn's layer is the classification: the failure arrives with a category, with the system named and with the original response attached, and invalid input is caught before it ever reaches a system. The retry is yours. Maesn retries and paces on the asynchronous endpoints, and on a synchronous call the target system's 429, 500 and 503 pass straight through for your application to act on. That same queue releases one tenant's requests in sequence, which is what keeps two token refreshes from overlapping and costing your customer a re-authentication.

When you do need to know what actually happened on the wire, the request, the response and every event around them are recorded, so debugging an intermittent failure is a matter of looking it up through logging and monitoring rather than reproducing it.

Why it matters

One failure path, every system

Building it yourself
  • Parse a different error shape per system
  • Guess which failures are retryable
  • Lose the original message on the way
  • Discover invalid input from the system
With Maesn
  • One envelope with a real category
  • Retry guidance per response code
  • Downstream errors passed through
  • Invalid input caught before sending

Error handling is where integration projects quietly lose their margin. Not in the first build, but in the second year, when every new system adds another failure dialect to a support runbook nobody wants to own, and a customer asks why their invoice did not sync. Because the envelope, the categories and the retry policy are Maesn's rather than each vendor's, that runbook is written once and stops growing. The system your customer picks next changes which logo is on the connection, not how your code handles a bad day. The runbook that stops growing is also the one customer success teams have to work from when a customer is already on the line.

Unified Error Handling FAQ

Common questions

What does a Maesn error look like?

Every error arrives in one top-level errors object holding a standardised type, the HTTP statusCode, a short human-readable message and a context block naming the target system, the Unified API and a timestamp. Depending on the type it also carries validationErrors or downstreamErrors. Because the shape never changes, one handler covers every system.

Why does the standardised type matter?

Because it lets you branch on the kind of failure instead of parsing prose. Invalid input, an expired connection, an unsupported endpoint, a rate limit and a temporary outage are all different situations that need different responses, and on a raw API they often arrive as the same status code with different wording. The type separates them the same way on every system.

What are validation errors?

Input problems Maesn detects before the request reaches the target system. Field formats, required values and data types are checked first, and anything wrong is listed in validationErrors with the field and a short message. You fix the named fields and resend, instead of learning about the problem from a downstream system in its own wording.

Do I lose the original error from the system?

No. When the error type is target_system_error, the system's own error response and HTTP status are passed through in downstreamErrors, verbatim. You get a normalised category to branch on and the raw provider detail to debug with, rather than one at the expense of the other.

Which errors should I retry?

429, 500 and 503 are worth retrying with exponential backoff. For 429 start with a one-minute delay, for 500 and 503 start with 15 to 30 seconds, and double the delay on each attempt until you hit a threshold where you treat the request as failed. Honour a Retry-After header when one is present. The 4xx codes that describe a problem with the request itself, such as 400, 401, 403, 404, 405 and 409, should not be retried, because the same request will fail the same way.

What happens when a target system has an outage?

A temporarily unavailable target endpoint surfaces as 503 with the service_unavailable type, which is the signal to back off and retry rather than treat the write as lost. Maesn learns about an outage from the vendor rather than from an alert of its own and tells the tenants running on that system, so it does not reach you as a silent failure. The backing off is yours to schedule on a synchronous call, and the retry along with it.

How would I know a value was ignored rather than rejected?

Through meta.warnings on the response. A call can succeed and still not do everything you asked: a field the target system does not use, or a value it ignores because it does not support it, is named there instead of disappearing. It is written for the developers integrating rather than for the end user, and it is what separates a write that worked from a write that worked the way you meant.

What if a connection expires?

You receive a 401 with the not_authorized type. Causes include a missing or wrong key, access revoked inside the target system, or expired tokens, and some systems cap the lifespan of a refresh token, thirty days for example, so re-authentication has to be part of the design rather than an edge case. Handle the 401 by prompting the customer to reconnect promptly.

How do I tell an unsupported endpoint from a broken one?

By the type. resource_not_supported means the endpoint is not implemented for that particular system, so retrying will never help and the answer is a different approach for that integration. A target_system_error means the call was valid but the system rejected or failed it, and the downstream detail tells you why.

Build once on the Unified API.

See how unified error handling works for your integration, or dive into the technical reference.