> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adipredictstreet.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Maintenance Notifications

> Know before the trading API goes down: poll the maintenance windows, or subscribe an address and be emailed.

## What this is

When we plan to take the trading API or withdrawals down, or take them down
for an incident we can announce, we declare a **maintenance window**. You can
find out two ways, and they are meant to be used together:

1. **Poll** [`GET /api/platform/maintenance`](#maintenance-windows) for
   upcoming and active windows. Requires a partner API key.
2. **Subscribe an address** and be emailed when a window is scheduled,
   rescheduled, started, extended, completed or cancelled. Ask your account
   manager; see [Email notifications](#email-notifications).

For "is it down right now", poll [`GET /api/platform/status`](#live-status).
It takes the same partner API key.

> [!NOTE]
>
> A window reports what we have **declared**, not what we have measured. A
> window is the right signal for planned works and announced incidents. It is
> not a health check — keep your own error handling for everything else.

***

## Maintenance windows

```http theme={null}
GET /api/platform/maintenance
X-Api-Key: ps_live_<keyId>_<secret>
```

Requires a **partner** API key in the `X-Api-Key` header — one we issued
when you were onboarded. No special scope is needed, any of your live keys
will do, and no `X-User-Wallet` header is needed: the schedule belongs to no
wallet.

| Response | When |
| - | - |
| `401 auth_required` | No credentials |
| `401 api_key_*` | The key is malformed, unknown, revoked, expired, or called from outside its IP allowlist |
| `403 api_key_required_not_jwt` | A logged-in user session (`Authorization: Bearer`) instead of a key |
| `403 partner_key_required` | A self-service key minted from a retail account (`POST /api/me/api-keys`) — the schedule of future outages is partner-only |
| `404 not_found` | The feature is not switched on in this environment yet |

Rate limits: 600 requests per minute per partner — all of your API keys share
that one bucket, and it scales with your partner rate tier like wallet limits
— plus 600 requests per minute per IP. Both buckets belong to this endpoint
alone: they do not count against the IP bucket `/api/platform/contracts` and
`/fee-config` share, or against the buckets of `/status` and `/notices`.
Responses carry `Cache-Control: private, max-age=5`, which matches
how fresh the data actually is — do not let an HTTP cache hold it longer
than that or you will read `scheduled` after the window has started.

### Query parameters

| Name | Type | Description |
| - | - | - |
| `from` | ISO-8601 | Return windows still relevant at this instant: everything live, plus anything that finished after it. Defaults to now, which gives upcoming + active. Pass your last sync time to catch up on windows that began and ended while you were not polling. Use a form with separators — `2026-10-11T02:00:00Z`, not `20261011`. Prefer `Z`; if you send an offset, URL-encode the `+` (`%2B`). |
| `scope` | enum | `trading`, `withdrawals` or `all`. Asking for `trading` or `withdrawals` **also returns `all` windows**, because an `all` window takes that surface down too. Asking for `all` returns full-platform windows only. Omit for every surface. |

### Response

```json theme={null}
{
  "windows": [
    {
      "id": "7f3c…",
      "scope": "trading",
      "status": "scheduled",
      "title": "Planned trading API maintenance",
      "message": "Order placement and cancellation will be unavailable. Existing orders are untouched.",
      "startsAt": "2026-10-11T02:00:00.000Z",
      "expectedEndAt": "2026-10-11T04:00:00.000Z",
      "startedAt": null,
      "endedAt": null
    }
  ],
  "from": "2026-10-10T12:00:00.000Z",
  "generatedAt": "2026-10-10T11:58:59.000Z"
}
```

### Polling incrementally

**Pass `generatedAt` back as your next `from`.** Not the `from` you sent, and
not your own clock.

```
poll 1:  GET /api/platform/maintenance              -> generatedAt: T1
poll 2:  GET /api/platform/maintenance?from=T1      -> generatedAt: T2
poll 3:  GET /api/platform/maintenance?from=T2      -> ...
```

`generatedAt` is deliberately about a minute behind the moment we read the
data. A window's end is stamped inside a transaction that commits a moment
later, and the lag guarantees that a completion or cancellation that was
mid-commit when we read still reaches you on the next poll. That means **you
will be shown windows you already have** for up to a minute after they
change — upsert by `id`, which is stable for the life of the window.

`from` in the response is only the filter that was applied to *this* request,
echoed for debugging. Using it as the cursor would move you past exactly the
transitions the overlap exists to protect, and a window that completed in
that gap would never appear again.

### Fields

| Field | Notes |
| - | - |
| `scope` | `trading`, `withdrawals`, or `all` — which covers both. |
| `status` | `scheduled`, `in_progress`, `completed`, `cancelled`. |
| `startsAt` | The announced start. |
| `expectedEndAt` | **Moves** while a window runs long. Re-read it rather than caching the first value. |
| `startedAt` | When it actually started; `null` until it does. |
| `endedAt` | When it actually ended, or when it was cancelled; `null` while open. |

### Three behaviours worth coding against

* **A running window stays listed after `expectedEndAt` passes.** Overrunning
  is exactly when you still need it. Drive your logic off `status`, never off
  the clock.
* **A window that is announced and never started disappears** once its
  `expectedEndAt` passes (until then it stays listed as `scheduled`, even
  after `startsAt`). It is not going to happen as announced; do not keep
  waiting for it. Every response lists every window that is still due or
  running, so treat a `scheduled` window you hold that is missing from a
  response as gone. If we later reschedule, start or cancel it, it reappears
  with the new status.
* **A window that has started is never cancelled** — it completes. Once you
  see `in_progress`, the only status that follows for that `id` is
  `completed`.

***

## Live status

```http theme={null}
GET /api/platform/status
X-Api-Key: ps_live_<keyId>_<secret>
```

The endpoint to poll for "can I trade right now".

Requires the same **partner** API key as the window feed, in the same header,
and likewise no `X-User-Wallet`. The `401` and `403` rows of the table above
apply. The `404` row does not: where the feature is off, `/status` still
answers and leaves out the `maintenance` object (see below).

Rate limits: 600 requests per minute per partner (all of your keys share it,
and it scales with your rate tier), plus 600 per minute per IP in one bucket
that `/status` and `/notices` share. Neither counts against the window feed or
against `/contracts` and `/fee-config`. Responses carry
`Cache-Control: private, max-age=5`.

> [!WARNING]
>
> **This has changed.** `/api/platform/status` and
> `/api/platform/notices` used to be public. They are not any more — a call
> without a key now gets `401`. If you have a freeze detector polling either of
> them anonymously, add the key you already hold. `/api/platform/contracts` and
> `/api/platform/fee-config` are unaffected and stay public.

```json theme={null}
{
  "frozen": false,
  "reason": null,
  "since": null,
  "activeNotices": [],
  "maintenance": {
    "trading":     { "status": "maintenance", "windowId": "7f3c…" },
    "withdrawals": { "status": "operational",  "windowId": null }
  }
}
```

| Value | What it means | What to do |
| - | - | - |
| `operational` | Nothing declared, and (for `trading`) order placement is within normal limits | Trade normally |
| `degraded` | Order placement is failing more than usual. Nobody switched anything off, and no end time has been promised | Retry; consider easing off your request rate |
| `maintenance` | Deliberately unavailable — a maintenance window is running, the platform-wide freeze is on, or (for `withdrawals`) withdrawals alone are frozen | Wait for the window to end rather than retrying hard |

> [!NOTE]
>
> The whole `maintenance` object is **absent** in an environment where the
> feature is switched off. Treat a missing object as "no information" — never as
> "everything is fine". The `frozen` / `reason` / `since` / `activeNotices`
> fields are always present.

`maintenance.<surface>.windowId` is the window explaining a `maintenance`
status — look it up in `GET /api/platform/maintenance` for the copy and the
expected end. It is `null` whenever no window explains the status: always
under `degraded`, because nobody declared it, and under `maintenance` when a
kill switch is on without an announced window. In both cases there is no
promised end to wait for.

Only `trading` is ever reported as `degraded`; it is the surface we measure.
`withdrawals` reports `operational` unless a window, the platform-wide
freeze, or the withdrawals-only freeze (`503 withdrawals_frozen`; `frozen`
stays `false`) covers it — so for withdrawals `operational` means "nothing
declared", not "measured healthy".

> [!NOTE]
>
> Treat `status` as an **open enum**. More values may be added, so branch on
> "is it `operational`" rather than matching the full set — anything else means
> do not expect normal service.

The `degraded` signal is the same alert that pages our own on-call. It is
derived from the share of order placements answered with a 5xx over a
rolling five-minute window, re-checked every 30 seconds. It needs a minimum
number of failures as well as a minimum share, so a handful of errors cannot
trip it, and it clears only after a recovery hold of about 15 minutes, so it
does not flap.

***

## Email notifications

### Who is already on the list

Every partner that was active when this list was created was subscribed
automatically, using the `contact_email` we hold for you. API integrators
were subscribed to `trading` and `withdrawals` (which include full-platform
windows); partners that embed our UI were subscribed to `all` only —
full-platform windows. You do not need to ask for that.

Partners onboarded since then are subscribed the same way, from the
`contact_email` on file, at the moment the account is created — so a new
partner is reachable from its first day rather than from the day somebody
remembers. Partners who joined before that was in place were added when it
went live, unless they already had an address on the list or had one
removed on request.

Two things follow from it being automatic:

* If that address is a personal mailbox or an alias nobody watches, **change
  it** — see below. It was the only address we had.
* If we hold no usable address for you, nothing is subscribed and you will
  hear nothing. If you have never had one of these emails, ask — do not assume
  silence means no maintenance.

### How much mail to expect

At most one email per event, per subscribed address. A normal release is
three — `scheduled`, `started`, `completed` — plus `extended` or
`rescheduled` only if something actually changes. Nothing is sent on a
repeat of the same transition, so a retry on our side does not produce a
duplicate. A notice that is already out of date when its turn comes is
dropped rather than sent late (for example, no `started` for a window that
has already completed), so a very short window can produce fewer emails. An
address that hard-bounces is taken off the list automatically.

What you hear about depends on which surfaces your address is subscribed to:

| Subscribed to | You are emailed about |
| - | - |
| `trading` | trading windows, **and** full-platform windows |
| `withdrawals` | withdrawals windows, **and** full-platform windows |
| `all` | full-platform windows **only** — nothing surface-specific |

`all` is narrower than it sounds: it means "only tell me when the whole
platform goes down". It is the right setting for an integration that embeds
our UI and never calls the trading API. If you want everything, subscribe to
`trading` and `withdrawals` — both already include full-platform windows.

### What each email says

You are emailed when a window is:

| Event | Subject begins | When |
| - | - | - |
| scheduled | `Planned maintenance on …` | We announce it, normally 24h+ ahead |
| rescheduled | `CHANGED: maintenance on … moved to …` | We move the announced times |
| rescheduled | `CHANGED: maintenance now affects …` | We change which surface it affects. If an earlier notice described a different surface, it no longer applies to it — this mail says so |
| started | `Maintenance started on …` | Works begin |
| extended | `Maintenance on … extended to …` | It is running longer than planned |
| completed | `Maintenance complete — … is available again` | Service is back |
| cancelled | `Planned maintenance on … cancelled` | We call it off before it starts |

Every email carries the **window ID**, so upsert by it on your side rather
than by subject line.

The plain-text part also names `GET /api/platform/status` as the endpoint to
poll for the current state. It is an API endpoint, not a page — opening it in
a browser gets you a `401`, because it needs your API key like every other
call. An email you missed therefore never leaves you without the current
state, but you read it with your client, not with a click.

### Adding, changing or removing an address

There is **no partner-facing API** for this, by design: the list decides who
receives operational notices in our name, so changes go through us.

| What you want | How |
| - | - |
| Add an address | Ask your account manager. More than one per partner is fine — `ops@`, `alerts@`, a named engineer |
| Change an address | Ask. An address is never edited in place; the old one is removed and the new one added, so the audit trail keeps saying what each one ever meant |
| Change which surfaces an address hears about | Ask |
| Stop mail to one address | Do it yourself — see below |

Tell us the mailbox your on-call actually watches, not a personal one.

### Unsubscribing

Every email carries an unsubscribe link, and the endpoints behind it are
public — the signed token in the link is the only credential. It stops mail
to that address for the partner account the email was sent for; if the same
mailbox is on the list for another account too, that subscription is
unaffected.

```http theme={null}
GET  /api/platform/maintenance/unsubscribe?token=<token>
POST /api/platform/maintenance/unsubscribe?token=<token>
```

`GET` **renders a confirmation page and changes nothing.** That is deliberate:
corporate mail gateways and link-preview bots fetch every URL in a message
before a human sees it, and a `GET` that unsubscribed would take your address
off the list without anyone clicking.

`POST` performs it. That is both the button on the page and
[RFC 8058](https://www.rfc-editor.org/rfc/rfc8058) one-click, which is what
the `List-Unsubscribe` and `List-Unsubscribe-Post` headers on every email
advertise — so your mail client's own "unsubscribe" works.

Unsubscribing is **one-way**: the token carries no re-subscribe bit, so a
leaked link can only stop mail. To go back on the list, ask.

***

## Errors you may see during maintenance

| Code | HTTP | Meaning |
| - | - | - |
| `trading_temporarily_unavailable` | 503 | Order placement could not be processed — the trading path is unreachable, or the exchange is too busy to take the order. Retry; do not treat it as a rejected order. `retryable`, when present, says whether resending can help. Resend the same signed order unchanged; do not sign a new one. |
| `shard_owner_unavailable` | 503 | The exchange node serving the account is restarting or being taken over. Nothing was attempted; resend the same request after `Retry-After` (2 seconds). Any order route can answer it, `cancel-all` included. |
| `cancel_pending` | 202 | A single cancel is recorded but the matching engine has not answered it yet. Send the same cancel again after `Retry-After` (5 seconds). In `cancel-batch` / `cancel-all` the same condition is the `notCancelled` reason `cancel_pending`. See [Cancelling orders](/concepts/trading/cancelling). |
| `exchange_unavailable` / `exchange_transport_error` | 503 | Any other trading call (for example a cancel) failed at the exchange or could not reach it; it may or may not have taken effect. Re-read, then retry with backoff. |
| `platform_frozen` | 503 | The platform-wide freeze is on (`frozen: true` on `GET /api/platform/status`). Placement and withdrawals are refused; single and batch cancels still work. |
| `withdrawals_frozen` | 503 | Withdrawals alone are frozen (`maintenance.withdrawals.status` = `maintenance`, `windowId` `null`). Trading is unaffected. |

The trading codes do not tell you *why*. That is what the maintenance window
is for.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.