---
title: "Place a pass window by hand"
method: POST
path: "/v1/projects/{project}/plans/{plan}/pass-windows"
tags: ["Pass Windows"]
---

# Place a pass window by hand

`POST /v1/projects/{project}/plans/{plan}/pass-windows`

## Request

- Base URL: `https://api.subscriby.net`
- URL: `https://api.subscriby.net/v1/projects/{project}/plans/{plan}/pass-windows`
- Auth: HTTP bearer

Places a window by hand on a `kind: pass` plan. This is how `schedule_mode: fixed` gets its dates, and how a repeating plan gets the one-off that does not fit its pattern.

```bash
curl -X POST https://api.subscriby.net/v1/projects/$PROJECT_ID/plans/$PLAN_ID/pass-windows \
  -H "Authorization: Bearer $SUBSCRIBY_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "starts_at": "2026-09-20 09:00",
    "duration_minutes": 180
  }'
```

Answers `201` with the new window: `status: scheduled`, `source: manual`, `holders: 0`. The request above and `"starts_at": "2026-09-20T09:00:00-04:00"` describe the same instant for a plan in `America/New_York`, and both come back as `"2026-09-20T13:00:00+00:00"`. Raises `pass.window_scheduled` with `source: manual`.

A manual window is never touched by regeneration: editing `pass.slots` on the plan rebuilds the generated windows and leaves this one where you put it. Every pass series with a rule is asked to look at the new date afterwards, so a matching series absorbs it as described on the [Plans](https://docs.subscriby.net/api/v1/reference/plans) page.

Each refusal below is a `422 VALIDATION_FAILED` naming the field, the same wording the dashboard's panel shows.

## Path parameters

- `project` string, uuid, required
- `plan` string, uuid, required

## Headers

- `Idempotency-Key` string, uuid, required

## Request body

- StorePassWindowRequest: A window to place by hand on a time-limited pass plan: when it starts and how long it lasts.
  - `starts_at` string, date-time, required: When access opens, ISO 8601. **A value with no offset is read as wall-clock time in the plan's `timezone`**, exactly as the dashboard authors it; a value with an offset or `Z` is the instant it names. Must be in the future, judged in the plan's own zone.
  - `duration_minutes` integer, required: How long the window lasts, a whole number of minutes, at least 1. The plan's configured floor and ceiling apply: 5 minutes and 30 days by default.

## Response `201`

The window.

- object
  - `data` PassWindowResource, required: A pass window: one dated stretch of access on a time-limited pass plan, generated from the plan's slots or placed by hand.
    - `id` string, required: The window's id, what a pass series points at.
    - `plan_id` string, required: The pass plan the window belongs to.
    - `plan_name` string, required: That plan's name, so a list reads without a plan lookup.
    - `starts_at` string, required: When access opens, ISO 8601 in UTC. Store and compare this.
    - `ends_at` string, required: When access closes, ISO 8601 in UTC.
    - `timezone` string, nullable, required: The IANA zone the plan's schedule was authored in. Render window times in this, never in UTC.
    - `local_range` string, required: The same window as a person reads it, already in `timezone`; what the dashboard, the portal and the bot all print.
    - `duration_minutes` integer, required: The length in minutes, derived from the two instants.
    - `status` string, required: `scheduled`, `open`, `closed` or `canceled`. Set by the scheduler as the window's start and end pass; `canceled` only by an explicit cancel.
    - `source` string, required: `generated` from the plan's slots, or `manual` when placed by hand. Manual windows survive a schedule rebuild; generated ones are regenerated.
    - `sellable` boolean, required: Whether a buyer could purchase this window right now: the plan's sales cutoff, resolved for the whole page in one query rather than per row.
    - `holders` integer, required: How many purchases currently hold the window.

## Other responses

- `400`: `IDEMPOTENCY_KEY_MISSING`: every write needs an `Idempotency-Key` header. Send a fresh UUID per distinct operation.
- `401`: `AUTHENTICATION_REQUIRED`: the request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an `sbt_test_` token on production).
- `403`: `TOKEN_MISSING_ABILITY`: the token is valid but does not carry the ability this endpoint requires; `error.context.required_ability` names the one to grant. An endpoint that also checks who owns a row or which tier the account is on answers `FORBIDDEN`, `TEAM_TIER_REQUIRED` or `CONNECTOR_TIER_REQUIRED` with the same status, and says so in its own description.
- `404`: `RESOURCE_NOT_FOUND`: an id in the path names nothing the token can see. `TENANT_MISMATCH`: the project sits outside the token's `scope:project:` allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred.
- `409`: `IDEMPOTENCY_KEY_REUSED`: the key was already used in the last 24 hours with a different request body.
- `422`: `VALIDATION_FAILED`: the payload broke a rule, and `error.fields` maps each offending key to its messages. A refusal from the domain, such as a plan that cannot go on sale or a member who cannot be removed, uses the same code with `error.message` saying why and no `fields`. On this endpoint: `VALIDATION_FAILED`: when the plan is not a time-limited pass: a subscription or a series has no windows of its own. `VALIDATION_FAILED`: when the start is in the past, judged in the plan's own zone. `VALIDATION_FAILED`: when the length is under the plan's floor or over its ceiling (5 minutes and 30 days by default). `VALIDATION_FAILED`: when the plan already has a window starting at that instant. `VALIDATION_FAILED`: when the plan stops selling a fixed number of minutes before a window **ends** and the window is not longer than that cutoff, so it would never be on sale while it runs.
- `425`: `IDEMPOTENCY_REPLAY_IN_PROGRESS`: the first request with this key is still running; retry in a few seconds and the original response is replayed.
- `429`: `RATE_LIMITED`: the token has spent its 300 requests a minute or 10,000 an hour; `Retry-After` says when the next one is accepted.

---

[API](https://skmtc.dev/memberpass/apis/subscriby-rest-api.md) · [All operations](https://skmtc.dev/memberpass/apis/subscriby-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/memberpass/apis/subscriby-rest-api/revisions/59c4df91d764?raw)
