A failing request is not an edge case; it is a normal state that a serious client spends most of its complexity handling. The difference between an API that is pleasant to build against and one that is not is usually how it describes failure.
The four answers a client needs
Almost every failure reduces to one of four instructions. Buy something, because you are not entitled. Buy something bigger, because your tier does not include this. Wait, because you have exceeded a ceiling. Or retry, because the fault is ours.
Those four lead to completely different code paths, and they are indistinguishable in prose. "Your subscription does not cover this" and "we could not verify your subscription right now" read almost identically to a human and mean opposite things to a program: one should stop and prompt a purchase, the other should retry in thirty seconds.
What a good error carries
A stable machine-readable code, separate from the human sentence. The sentence can be rewritten for clarity at any time; the code must not change, because somebody is branching on it. A retry hint when retrying is appropriate. And a link or path to where the problem is fixed.
The codes should also be documented somewhere public, and the documentation should be complete. A code that appears in production and not in the docs is one your client will hit and not recognise.
The quiet failure to avoid
Worse than a bad error is a successful one. Returning HTTP 200 with an empty array when the upstream source failed tells a client that the market is empty, which is a factual claim and a false one. A failure should look like a failure.