# Getting started

Nifty has two REST APIs on one host, `https://openapi.niftypm.com`:

| | Use it when | Base path | Status |
| --- | --- | --- | --- |
| **v3** | you are starting today | `/api/v3` | Current. Cursor pagination, `expand`, idempotency keys, RFC 7807 errors. |
| **v1.0** | you maintain an existing integration | `/api/v1.0` | Legacy. Frozen; stops serving 1 September 2027. See [Migrating](#migrating-from-v1). |

Both accept the same bearer token.

## Your first request in four steps

1. **Create a token.** In Nifty go to **Settings → API Tokens → New token**, choose the scopes your integration needs, and copy the token. It is shown once and looks like `nft_user_1a2b3c4d_<48 characters>_9f8e`.
2. **Export it.**
   ```bash
   export NIFTY_TOKEN=nft_user_...
   ```
3. **List your projects.**
   ```bash
   curl "https://openapi.niftypm.com/api/v3/projects?limit=2" \
     -H "Authorization: Bearer $NIFTY_TOKEN"
   ```
4. **Read the response.** Every list returns the same envelope:
   ```json
   { "data": [ { "id": "Pz2J1w3rkD", "name": "Website relaunch", "niceId": "WEB", "…": "…" } ],
     "total": null, "limit": 2, "hasMore": true, "nextCursor": "eyJpZCI6IiFZekJ…" }
   ```
   Pass `cursor=<nextCursor>` to get the next page; `total` is `null` unless you send `includeTotal=true`.

Authentication failures answer `401` with
`{"type":"unauthorized","code":"unauthorized"}`. The body does not distinguish a
missing header, a revoked token and a truncated one. Verify the token you sent
matches the token you copied.

A `403` on a v1.0 route means the token does not carry a scope that route
accepts. See
[Authentication](/authentication#scope-requirement-for-the-legacy-rest-api).

## Create something

```bash
curl -X POST https://openapi.niftypm.com/api/v3/tasks \
  -H "Authorization: Bearer $NIFTY_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c8e2a-first-task" \
  -d '{ "projectId": "Pz2J1w3rkD", "name": "Ship the API quickstart" }'
```

`201` returns the task. Send the same `Idempotency-Key` again within ten
minutes and you get the same task back instead of a duplicate. Read relations
with `?expand=assignees,labels` on any `GET`.

## Concepts you will meet everywhere

- **Ids** are short opaque strings (`Pz2J1w3rkD`), never numeric — never parse or construct one. Actor ids are UUIDs.
- **Timestamps** are ISO 8601 in UTC.
- **Errors** are RFC 7807 with a `code` to branch on and a `docsUrl` — see [Errors](/errors).
- **Concurrency:** send `expectedUpdatedAt` on `PATCH` to get a `409 stale_write` instead of overwriting someone else's change.
- **Soft delete:** on the resources that support it, `DELETE` hides the record and schedules it for purge; `POST /{resource}/{id}/restore` brings it back until then.
- **Rate limits:** see [Rate limits](/rate-limits).

## Migrating from v1

v1 responses are snake_case and path-versioned; v3 is camelCase, one host, one
envelope. Most resource names map 1:1, but not all of them:

| v1.0 | v3 |
| --- | --- |
| `subteams` | `portfolios` |
| `taskgroups` | `statuses` |
| `docs` | `documents` |
| `milestones` | `lists` |
| `fields` | `custom-fields` |
| `time` | `time-entries` |
| `labels` | `labels` (the UI calls them Tags) |

`templates`, `apps`, `users` and `invites` have **no v3 counterpart** — stay on
v1.0 for those. `webhooks` is published on both; the
[Webhooks](/webhooks) guide documents the v3 surface.
