---
updatedAt: 2026-10-01T22:27:55.000Z
agentTools:
  projectIndex: https://developer.sovrn.com/llms.txt
---

# Errors

Error response shapes and how to handle them.

Every **JSON** error carries a human-readable `message`: key off it. Two
responses are not JSON. The `401` is a bare `text/plain` string, because the
API key is validated at Sovrn's gateway before the request reaches the
service, and a `500` is the plain text `Internal Server Error`. Handle these
cases:

| Status | Meaning                                                                                                  | Body                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------ | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Validation failed                                                                                        | Either the validation shape, whose `message` starts with the path of the field, or `{ "message": "<reason>" }`. See [Validation errors](#validation-errors).                                                                                                                                                                                                                                             |
| `400`  | The deal request id in the path is malformed                                                             | The validation shape, with the `message` `params/id Invalid deal request ID`                                                                                                                                                                                                                                                                                                                             |
| `401`  | API key missing, invalid, or malformed                                                                   | **`text/plain`, not JSON.** One of three strings: `api key not provided` (no `x-api-key` header), `invalid api key` (unknown, revoked, or expired key), `invalid api key syntax` (malformed key)                                                                                                                                                                                                         |
| `403`  | Key valid but not permitted                                                                              | `{ "message": "Insufficient scope" }`: a read-only key sent a write (`POST` or `PATCH`, a dry run included). It is checked before the body is validated, so it arrives even when the body is invalid too. `{ "message": "Partner not authorized for this API" }`: the account is not enabled for the API. On a write it is checked after the body is validated, so an invalid body gets its `400` first. |
| `404`  | No deal request with that id on your account                                                             | `{ "message": "Deal request not found" }`                                                                                                                                                                                                                                                                                                                                                                |
| `415`  | A `PATCH` was sent as `application/merge-patch+json`                                                     | `{ "statusCode": 415, "code": "FST_ERR_CTP_INVALID_MEDIA_TYPE", "error": "Unsupported Media Type", "message": "Unsupported Media Type" }`. Send a `PATCH` body as `application/json`.                                                                                                                                                                                                                    |
| `429`  | The key went over the [rate limit](https://developer.sovrn.com/docs/introduction#rate-limit)                                          | JSON, with a `message` such as `Rate limit exceeded, retry in 45 seconds`. Wait that long before sending again.                                                                                                                                                                                                                                                                                          |
| `500`  | The downstream deal system could not be reached or did not answer in time, or another unexpected failure | **`text/plain`, not JSON:** `Internal Server Error`. See [Retrying](#retrying).                                                                                                                                                                                                                                                                                                                          |
| `502`  | The downstream deal system rejected the request, or returned a result the API could not use              | `{ "message": "…", "upstream": "vulcan" }`. See [Retrying](#retrying).                                                                                                                                                                                                                                                                                                                                   |

## Retrying

> 🚧 A 5xx does not mean nothing happened
>
> A `502` or a `500` from a write does not tell you the request left nothing
> behind, so what to do depends on the request:
>
> * **Creating a deal** (`POST /deal-requests`): the deal may already exist
>   on Sovrn's side with no deal request recorded for it. Check with your
>   Sovrn representative before you retry, or you may create it twice.
> * **Editing a deal** (`PATCH /deal-requests/{id}`): if the request set a
>   `status`, the other fields may already be applied. Re-send the same
>   request until it returns `200`; a `PATCH` sets the same values each time
>   it is sent.
> * **An archived deal** refuses every edit with a `502`, so re-sending will
>   not help (see [Deal lifecycle](https://developer.sovrn.com/docs/deal-lifecycle#changing-a-deals-status)).
>   If you were archiving the deal and the re-send keeps returning `502`, read
>   the deal: if it reads `archived`, the archive has already landed.
>
> Do not `JSON.parse` a `401` or a `500`: their bodies are plain strings.

## Validation errors

A `400` arrives in one of two shapes.

When a field fails the request's schema, the body carries `statusCode`,
`code` and `error` beside a `message` that starts with the path of the field
(`body/…`, `params/…` or `querystring/…`):

```json
{
  "statusCode": 400,
  "code": "FST_ERR_VALIDATION",
  "error": "Bad Request",
  "message": "body/commercial/takeRate takeRate is a decimal fraction; expected 0 ≤ x ≤ 0.85"
}
```

When several fields fail, their messages arrive together in the one
`message`, separated by commas. To recognize a particular error, match on the
text after the path.

A rule the API checks against the deal as stored, such as editing the buyers
of an open deal or a DV360 requirement on a `PATCH`, returns `message` only:

```json
{
  "message": "buyers cannot be edited on an open deal; create a new deal request instead"
}
```

Messages you may see, shown without their path: `takeRate is a decimal
fraction; expected 0 ≤ x ≤ 0.85`, `startDate must be today or later (UTC)`,
`runOfNetwork and targeting are mutually exclusive; provide one or the other`,
`at most 25 targeting groups per deal request`, `unknown audience segment
id(s): …`, `country code(s) not available for targeting: …`,
`device.deviceType value(s) not available for targeting: …`,
`publisher id(s) not valid: …`,
`schedule.endDate is required for DV360 buyers` (see
[Demand partners](https://developer.sovrn.com/docs/demand-partners#dv360-requirements)).

## Validation is not uniform across targeting values

**Only `device.geo.country`, `device.geo.region`, `device.language`,
`device.deviceType`, `audience.segmentids`, `content.categorization.v3`,
`content.categorization.v1` and `publisher.id` values are checked when you
submit**, on a dry run too: every attribute whose `valueFormat` is
`enumerated`, except `adUnit.size`, plus `publisher.id`. A value that is not
on its attribute's
[`GET /targeting-attributes/{attribute}/values`](https://developer.sovrn.com/docs/targeting#discovering-attributes-and-values)
list returns a `400` listing the values in question, and so does a publisher
id that is not a positive whole number up to `2147483647`. A 2-letter country
code such as `US` is not rejected: it is converted to its 3-letter code, `USA`
(see [Targeting](https://developer.sovrn.com/docs/targeting#attribute-reference)).

Worth knowing when debugging a deal that was accepted but isn't delivering:
the other free-text attributes accept any value of the right JSON type, and
so do ad sizes, although they have a list. A malformed ad-size token produces
a deal that is created successfully and then never matches traffic, with no
error at any point. See [Targeting](https://developer.sovrn.com/docs/targeting) for the exact format each
attribute expects.

## Open deals

Two `400` messages are specific to `type: open` deals (see
[Deal lifecycle](https://developer.sovrn.com/docs/deal-lifecycle#deal-types)), and both are exact strings you
can match on:

| When                                                   | `message`                                                                    |
| ------------------------------------------------------ | ---------------------------------------------------------------------------- |
| A buyer on an `open` deal request lists a seat         | `open deals are non-seat: omit seatIds — each buyer carries the DSP only`    |
| A `PATCH` on an existing `open` deal includes `buyers` | `buyers cannot be edited on an open deal; create a new deal request instead` |

The first arrives in the validation shape, prefixed with the field path
(`body/buyers/0/seatIds …`); the second is a plain `{ "message": "…" }` body.
Neither is retryable as sent: fix the request.

Every buyer in a `PATCH` must list at least one seat, and that is checked
before the open-deal rule. So a `PATCH` on an open deal whose buyer has no
`seatIds` gets the seat message first,
`body/buyers/0/seatIds Invalid input: expected array, received undefined`,
and not the second message above.

## Setting a status

Two more `400` messages come from `PATCH`ing a `status` (see
[Deal lifecycle](https://developer.sovrn.com/docs/deal-lifecycle#changing-a-deals-status)):

| When                                                            | `message`                                                                                                                   |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| The value is outside the four a curator may set                 | `body/status Invalid option: expected one of "active"\|"paused"\|"archived"\|"scheduled"`                                   |
| Every buyer on the deal is on a DSP that manages its own status | `status is not settable on this deal: every buyer is on a DSP that manages deal status through its own integration (dv360)` |

The second names the DSPs on the deal that blocked the change, so it varies
with the deal. Neither is retryable as sent. To avoid the second, read the
deal's `statusSettable` first and offer a status change only when it is
`true`; see
[Deal lifecycle](https://developer.sovrn.com/docs/deal-lifecycle#ask-the-deal-not-the-dsp).