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

# Quickstart

> From credentials in hand to a successful /v1/me call in 15 minutes.

This walks you from "I have credentials" to "I can read a crew member's profile"
in about 15 minutes. We use the sandbox throughout. The flow is identical in
production — swap the base URL.

<Steps>
  <Step title="Have your sandbox credentials handy">
    During onboarding you'll have received:

    * `client_id` (e.g. `ywc_a1b2c3d4`)
    * `client_secret`
    * `webhook_secret` (only if you registered a webhook URL)
    * One or more registered `redirect_uri`s

    If you don't have these yet,
    [email us](mailto:admin@digitalseaservice.com) — sandbox is issued
    same-day.
  </Step>

  <Step title="Generate a PKCE pair">
    Every authorization request needs a fresh `code_verifier` /
    `code_challenge` pair. The verifier stays in your server; the challenge
    goes on the wire. See [Authentication](/authentication#pkce) for samples
    in other languages.

    <CodeGroup>
      ```bash cURL theme={null}
      CODE_VERIFIER=$(python -c 'import secrets;print(secrets.token_urlsafe(64)[:64])')
      CODE_CHALLENGE=$(python -c "
      import base64, hashlib, sys
      v = sys.argv[1]
      print(base64.urlsafe_b64encode(hashlib.sha256(v.encode()).digest()).rstrip(b'=').decode())
      " "$CODE_VERIFIER")
      echo "verifier=$CODE_VERIFIER"
      echo "challenge=$CODE_CHALLENGE"
      ```

      ```python Python theme={null}
      import base64, hashlib, secrets

      code_verifier = secrets.token_urlsafe(64)[:64]
      code_challenge = base64.urlsafe_b64encode(
          hashlib.sha256(code_verifier.encode()).digest()
      ).rstrip(b"=").decode()
      ```

      ```javascript Node theme={null}
      import { createHash, randomBytes } from "node:crypto";

      const codeVerifier = randomBytes(48).toString("base64url").slice(0, 64);
      const codeChallenge = createHash("sha256")
        .update(codeVerifier)
        .digest("base64url");
      ```
    </CodeGroup>

    Store the `code_verifier` against the user's session — you'll need it in
    step 4.
  </Step>

  <Step title="Send the crew member to /oauth/authorize">
    Build this URL and redirect the user's browser to it. The DSS dashboard
    handles sign-in and shows them a consent screen listing the scopes you
    requested. On approval they're redirected back to your `redirect_uri`
    with a one-shot `code` and the `state` you passed.

    ```
    https://api.dev.digitalseaservice.com/oauth/authorize?response_type=code
      &client_id=YOUR_CLIENT_ID
      &redirect_uri=https://yourapp.example.com/integrations/dss/callback
      &scope=profile:read+seatime:read+vessels:read
      &state=RANDOM_PER_REQUEST_CSRF_TOKEN
      &code_challenge=GENERATED_IN_STEP_2
      &code_challenge_method=S256
    ```

    <Warning>
      Validate `state` on the callback before doing anything else. If it
      doesn't match the value you sent, drop the request — it's the standard
      defence against CSRF on the OAuth dance.
    </Warning>
  </Step>

  <Step title="Exchange the code for tokens">
    The user's browser hits your `redirect_uri` with `?code=...&state=...`.
    Your server exchanges the code for an access token + refresh token.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -sS -X POST https://api.dev.digitalseaservice.com/oauth/token \
        -d grant_type=authorization_code \
        -d code="$CODE" \
        -d redirect_uri=https://yourapp.example.com/integrations/dss/callback \
        -d client_id="$CLIENT_ID" \
        -d client_secret="$CLIENT_SECRET" \
        -d code_verifier="$CODE_VERIFIER"
      ```

      ```python Python theme={null}
      import httpx

      r = httpx.post(
          "https://api.dev.digitalseaservice.com/oauth/token",
          data={
              "grant_type": "authorization_code",
              "code": code,
              "redirect_uri": "https://yourapp.example.com/integrations/dss/callback",
              "client_id": CLIENT_ID,
              "client_secret": CLIENT_SECRET,
              "code_verifier": code_verifier,
          },
          timeout=10,
      )
      r.raise_for_status()
      tokens = r.json()
      ```

      ```javascript Node theme={null}
      const res = await fetch(
        "https://api.dev.digitalseaservice.com/oauth/token",
        {
          method: "POST",
          headers: { "content-type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            grant_type: "authorization_code",
            code,
            redirect_uri: "https://yourapp.example.com/integrations/dss/callback",
            client_id: CLIENT_ID,
            client_secret: CLIENT_SECRET,
            code_verifier: codeVerifier,
          }),
        }
      );
      if (!res.ok) throw new Error(`token exchange failed: ${res.status}`);
      const tokens = await res.json();
      ```
    </CodeGroup>

    Response:

    ```json theme={null}
    {
      "access_token": "...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "...",
      "scope": "profile:read seatime:read vessels:read"
    }
    ```

    Store the tokens against the user record on your side. **The
    `refresh_token` rotates on every use** — when you refresh, swap the stored
    value for the new one immediately. See
    [Authentication / refreshing](/authentication#refreshing-tokens).
  </Step>

  <Step title="Call /v1/me">
    You now have an access token. Try it against the simplest endpoint.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
        https://api.dev.digitalseaservice.com/v1/me
      ```

      ```python Python theme={null}
      import httpx

      r = httpx.get(
          "https://api.dev.digitalseaservice.com/v1/me",
          headers={"Authorization": f"Bearer {access_token}"},
          timeout=10,
      )
      r.raise_for_status()
      me = r.json()
      ```

      ```javascript Node theme={null}
      const res = await fetch(
        "https://api.dev.digitalseaservice.com/v1/me",
        { headers: { Authorization: `Bearer ${accessToken}` } }
      );
      if (!res.ok) throw new Error(`me failed: ${res.status}`);
      const me = await res.json();
      ```
    </CodeGroup>

    Successful response:

    ```json theme={null}
    {
      "user_id": "65a1f0e2c3b4d5e6f7a8b9c0",
      "name": "Alex Crew",
      "role": "Mate",
      "country": "GB",
      "photo_url": "https://cdn.digitalseaservice.com/u/65a1f0e2.jpg",
      "record_updated_at": "2026-05-22T14:21:00Z",
      "membership": {
        "tier": "premium",
        "source": "sponsored",
        "since": "2026-07-25",
        "expires_at": "2027-07-25T09:14:00Z",
        "sponsored_by_you": true
      }
    }
    ```

    If you see this — you're integrated.

    The `membership` block tells you whether this crew member already holds
    Premium. Gate any upgrade prompt on it — see
    [Membership status](/membership).
  </Step>
</Steps>

## What to build next

In rough order of value:

1. **Render the data in your UI.** Pull `/v1/me/sea-time` for the dashboard
   totals and `/v1/me/vessels` for the history list. Each returns
   `record_updated_at` and supports conditional GETs — use them.
2. **Wire conditional GETs.** Cache the `ETag` and replay it on the next
   request as `If-None-Match`. A `304` costs you nothing against your rate
   limit budget beyond a single round trip.
3. **Register a webhook URL** so you don't have to poll.
   See [Webhooks](/webhooks/overview).
4. **Add the disconnect path.** Call `POST /oauth/revoke` with the user's
   token when they want to disconnect from your side (revoking one token
   revokes the whole pair), and handle `user.consent.revoked` webhooks for
   when they disconnect from ours.

## Common gotchas

<AccordionGroup>
  <Accordion title="invalid_grant on token exchange">
    Three causes account for \~all of these. The authorization code expired
    (60-second TTL — exchange immediately), the code was already used (codes
    are single-use), or the `redirect_uri` you passed at token exchange
    doesn't exactly match the one you passed at authorize. Trailing slashes
    count.
  </Accordion>

  <Accordion title="invalid_token after a few hours">
    Access tokens are 1-hour TTL. Refresh them with
    `grant_type=refresh_token`. The previous access + refresh pair is revoked
    atomically when you refresh — if you keep using the old refresh token,
    you'll lose the connection and the user will have to reconsent.
    See [Authentication / refreshing](/authentication#refreshing-tokens).
  </Accordion>

  <Accordion title="insufficient_scope">
    Your access token was issued without the scope this endpoint needs. Send
    the user back through the authorize flow with the missing scope
    requested. Refresh cannot widen scope by design.
  </Accordion>
</AccordionGroup>

## Going to production

Once your sandbox integration is working end-to-end (auth, all four read
endpoints, at least one webhook handled), email
[admin@digitalseaservice.com](mailto:admin@digitalseaservice.com)
to schedule the integration review. We check your redirect URI is HTTPS, your
webhook handler verifies signatures correctly, and that you've covered the
disconnect path. Production credentials are issued same-day on approval.


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