> ## Documentation Index
> Fetch the complete documentation index at: https://docs.testnet.dev.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", [`GET /api/platform/status`](#live-status) is
public and needs no 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.
</Note>

***

## 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 and do not count against the limit the other `/api/platform/*` routes
share. 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
```

Public, no key. This is the endpoint to poll for "can I trade right now".

```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.
</Note>

`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.
</Note>

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.

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.
* A partner onboarded *after* the list was created is **not** added
  automatically. If you joined recently and have never had one of these
  emails, ask.

### 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** and a link to `GET /api/platform/status`,
so an email you missed never leaves you without the current state. Upsert by
window ID on your side.

### 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.