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

# Authentication

> Every Daysync Integration API call requires two layers of authentication — partner credentials and a Daysync user token.

Every API call requires **both** authentication layers. Missing or invalid credentials result in a `401` or `403` — see [Errors](/errors).

| Header                          | Purpose                                       | Source                                        |
| ------------------------------- | --------------------------------------------- | --------------------------------------------- |
| `x-api-key`                     | Identifies your partner application           | Provisioned by Daysync                        |
| `x-api-secret`                  | Authenticates your partner application        | Provisioned by Daysync (stored bcrypt-hashed) |
| `Authorization: Bearer <token>` | Identifies the Daysync user the call acts for | OAuth flow (below)                            |

* The **partner key/secret** is a long-lived credential issued to your app. Treat the secret like a password: it is shown once at provisioning time and stored only as a bcrypt hash, so it cannot be recovered — only rotated.
* The **user token** is a short-lived (≈1 hour) OAuth access token that represents the Daysync user who authorized your app. All data access is scoped to what that user can see and do in Daysync.

<Note>
  The partner credential carries your granted **scopes**, which gate endpoint access. See [Scopes & Permissions](/scopes).
</Note>

## Partner credentials

Partner credentials are issued through the [Daysync Integrations Portal](https://integrations.daysync.com). A credential has:

* an **API key** (`dk_live_…`) sent in `x-api-key`,
* an **API secret** (`ds_…`) sent in `x-api-secret`,
* a set of **scopes**,
* an optional **expiry**.

A credential is rejected if it is unknown, revoked, or expired. Note that a **present-but-invalid** API key is rejected by the gateway authorizer with `403 Forbidden` (a bare `{ "message": "Forbidden" }` body) — only a **missing** API key returns `401`. See [Errors](/errors).

### Obtaining credentials

Apply for partner credentials in the [Integrations Portal](https://integrations.daysync.com). Once your application is approved you receive:

| Field      | Example                                                          |
| ---------- | ---------------------------------------------------------------- |
| API Key    | `dk_live_…`                                                      |
| API Secret | `ds_…`                                                           |
| Scopes     | The set of scopes your integration needs (see [Scopes](/scopes)) |
| Expires    | Credential expiry date (if applicable)                           |

<Warning>
  Partner credentials are shown **once** at provisioning time and stored as a bcrypt hash. They cannot be recovered — only rotated. Keep them in a secure vault.
</Warning>

## User token — OAuth 2.0 Authorization Code flow (with PKCE)

Daysync users authorize your app through the **Daysync Integrations Portal**: a branded Daysync sign-in followed by a consent screen listing the permissions you request. Your backend then exchanges the returned authorization code for tokens.

Your existing partner credential doubles as your OAuth client — there is no separate OAuth registration:

| Setting               | Value                                                                             |
| --------------------- | --------------------------------------------------------------------------------- |
| Authorize URL         | `https://integrations.daysync.com/authorize`                                      |
| Token URL             | `https://integrations.daysync.com/api/oauth/token`                                |
| Client ID             | Your **API key** (`dk_live_…`)                                                    |
| Client secret         | Your **API secret** (`ds_…`)                                                      |
| Grant type            | Authorization Code **with PKCE (S256 — required)**                                |
| Redirect URI          | A callback URI **you host**, registered self-service on your app page (see below) |
| Client authentication | HTTP Basic auth header (`api_key:api_secret`)                                     |

<Note>
  **PKCE is mandatory.** Every authorize request must carry `code_challenge` (S256) and every token exchange the matching `code_verifier`. Requests without S256 PKCE are rejected with `invalid_request`.
</Note>

### Redirect (callback) URI

The OAuth flow **requires** a redirect URI — it's where Daysync sends the user after they approve access, appending the authorization `?code=…` that your backend exchanges for tokens.

**Register it yourself, instantly.** Open your app in the [Integrations Portal](https://integrations.daysync.com), add your callback under **Redirect URIs**, and save — the allowlist takes effect immediately, no Daysync review needed. HTTPS is required (`http://localhost` is permitted for development). An authorize request whose `redirect_uri` is not on your app's list is rejected before any redirect happens ("This application's redirect URL is not registered").

<Warning>
  The authorization `code` is delivered to whatever URL you pass as `redirect_uri`, so only a URL **you control** lets your backend capture the `code` and exchange it for an access token.
</Warning>

### Requestable scopes

Request scopes space-separated in the `scope` parameter — ask only for what you need. The two OpenID scopes plus the 14 Daysync API scopes are available:

```text theme={null}
openid
email

daysync-api/accommodation.read    daysync-api/accommodation.write
daysync-api/bulletins.read        daysync-api/bulletins.write
daysync-api/guestlist.read        daysync-api/guestlist.write
daysync-api/organization.read
daysync-api/schedule.read         daysync-api/schedule.write
daysync-api/tours.read            daysync-api/tours.write
daysync-api/users.read
daysync-api/venues.read           daysync-api/venues.write
```

<Note>
  The consented scope is clamped to **what your partner credential is granted**: scopes you request beyond your credential's grant are silently dropped from the consent screen and the issued token. Endpoint access is ultimately gated by your **partner** scopes (see [Scopes & Permissions](/scopes)).
</Note>

### Step 1 — Redirect the user to the portal

Generate a PKCE pair (`code_verifier` = random 43–128 chars; `code_challenge` = base64url(SHA-256(verifier))), then send the user to:

```text theme={null}
https://integrations.daysync.com/authorize
  ?response_type=code
  &client_id=dk_live_YOUR_API_KEY
  &redirect_uri=YOUR_REDIRECT_URI
  &scope=openid+email+daysync-api/tours.read+daysync-api/organization.read
  &state=RANDOM_STATE
  &code_challenge=YOUR_CODE_CHALLENGE
  &code_challenge_method=S256
```

The user signs in with their normal Daysync account, then sees a consent screen naming your app and the requested permissions.

### Step 2 — Receive the authorization code

After the user approves, the browser is redirected to your `redirect_uri` with `?code=AUTH_CODE&state=…` appended. If they decline, you receive `?error=access_denied` instead. Codes are short-lived (5 minutes) — exchange promptly.

### Step 3 — Exchange the code for tokens

Authenticate the client with HTTP Basic auth (`-u`) using your **API key and secret**, and include the PKCE `code_verifier`:

```bash theme={null}
curl -X POST https://integrations.daysync.com/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -u "dk_live_YOUR_API_KEY:ds_YOUR_API_SECRET" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=YOUR_REDIRECT_URI" \
  -d "code_verifier=YOUR_CODE_VERIFIER"
```

Response:

```json theme={null}
{
  "access_token": "eyJraWQ...",
  "refresh_token": "eyJjdHk...",
  "token_type": "Bearer",
  "expires_in": 3412,
  "scope": "daysync-api/tours.read daysync-api/organization.read"
}
```

Use the **`access_token`** as your `Authorization: Bearer` token, and treat `scope` as the authoritative record of what the user consented to.

<Note>
  `expires_in` reflects the **actual remaining life** of the issued token and can be shorter than the nominal hour. Track it and refresh when it approaches zero rather than assuming 3600.
</Note>

### Step 4 — Refresh the token

Access tokens expire after ≈**1 hour**; the **refresh token is valid for 30 days**. Exchange a valid refresh token for a new access token (no user interaction needed):

```bash theme={null}
curl -X POST https://integrations.daysync.com/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -u "dk_live_YOUR_API_KEY:ds_YOUR_API_SECRET" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=<REFRESH_TOKEN>"
```

The response has the same shape as Step 3 (no `scope` field). If the refresh token itself has expired, you receive `400 invalid_grant` — restart the flow from Step 1.

## Calling the API

Once you have an access token, every API call needs the bearer token **and** your partner key/secret:

```bash theme={null}
curl -i "https://integration.daysync.com/v1/tours?org_id=<ORG_ID>" \
  -H "Authorization: Bearer <access_token>" \
  -H "x-api-key: <API_KEY>" \
  -H "x-api-secret: <API_SECRET>"
```

## Postman setup

| Field                 | Value                                                               |
| --------------------- | ------------------------------------------------------------------- |
| Type                  | OAuth 2.0                                                           |
| Grant Type            | Authorization Code (With PKCE)                                      |
| Auth URL              | `https://integrations.daysync.com/authorize`                        |
| Access Token URL      | `https://integrations.daysync.com/api/oauth/token`                  |
| Client ID             | Your API key (`dk_live_…`)                                          |
| Client Secret         | Your API secret (`ds_…`)                                            |
| Code Challenge Method | SHA-256                                                             |
| Scope                 | `openid email daysync-api/tours.read daysync-api/organization.read` |
| Client Authentication | Send as Basic Auth header                                           |

Remember to add your Postman callback URL (`https://oauth.pstmn.io/v1/callback`) to your app's **Redirect URIs** in the portal first. After Postman completes the OAuth exchange, set the obtained `access_token` as a Bearer token and add your `x-api-key` / `x-api-secret` headers, then call the API.

## How the token is validated

The API verifies the bearer token on every call:

* signature against the published JWKS,
* issuer matches the Daysync user pool,
* `token_use` is `access`,
* token has not expired.

A failed check returns `401`.

## CORS

The API answers `OPTIONS` preflight requests with `204` before any authentication, and successful API responses include `Access-Control-Allow-Origin: *`.

<Warning>
  **Browser (cross-origin) clients are not currently supported.** Two gateway issues block calling the API directly from a browser:

  * The preflight's `Access-Control-Allow-Headers` lists `authorization`, `content-type`, and `x-api-key` but **not `x-api-secret`** — a header every call must send. The browser blocks the request at preflight.
  * When a preflight includes an `Access-Control-Request-Headers` header (which browsers always send when requesting custom headers like `x-api-key`/`x-api-secret`), the API returns `204` with **no `Access-Control-Allow-*` headers at all**, so the browser blocks the actual request.

  To continue use of the API, call the API **server-to-server** rather than from a browser. (Sending the partner `x-api-secret` from browser code is also poor practice.)
</Warning>
