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

# Get started

> Add a charger from your app: one endpoint on your server, a few lines in your app, and a check on your server afterwards.

export const Screen = ({name, alt, caption, wide = false}) => {
  const style = wide ? {
    width: '100%'
  } : {
    width: '280px',
    maxWidth: '100%'
  };
  return <figure style={{
    margin: '1.5rem 0',
    textAlign: 'center'
  }}>
      <img className="block dark:hidden" src={`/images/sdk/${name}-light.webp`} alt={alt} style={{
    ...style,
    margin: '0 auto',
    borderRadius: '12px',
    border: '1px solid rgba(128, 128, 128, 0.25)'
  }} />
      <img className="hidden dark:block" src={`/images/sdk/${name}-dark.webp`} alt={alt} style={{
    ...style,
    margin: '0 auto',
    borderRadius: '12px',
    border: '1px solid rgba(128, 128, 128, 0.25)'
  }} />
      {caption ? <figcaption style={{
    marginTop: '0.5rem',
    fontSize: '0.875rem',
    opacity: 0.7
  }}>{caption}</figcaption> : null}
    </figure>;
};

<Note>
  The Plugchoice SDK is in beta. Until version 1.0, a minor version may change the SDK or the SDK API. Pin the SDK version in your app and the `Plugchoice-Version` header on your server.
</Note>

This guide adds a charger end to end, in test mode. You write one endpoint on your server, open Link from your app, and confirm the outcome on your server.

## Before you start

* **Test credentials** for the SDK API: a client ID and secret. Email [support@plugchoice.com](mailto:support@plugchoice.com) to get them.
* **The SDK in your app**: follow [Installation](/sdk/install) and the configuration on your platform's page ([iOS](/sdk/ios), [Android](/sdk/android), [React Native](/sdk/react-native)).
* **A charger to add**: a Volt Time, Zaptec, Alfen or Peblar charger, or any other OCPP 1.6 charger. Test mode works with real chargers; see [Test and live](/sdk/concepts/test-and-live).
* **A phone**: Wi-Fi hotspots, the local network and Bluetooth don't work in a simulator.

## 1. Get an access token

Your server exchanges its client ID and secret for an access token, which lasts an hour:

```bash theme={null}
curl -X POST https://app.plugchoice.com/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET
```

Reuse the token until it expires. In Node:

```ts plugchoice.ts theme={null}
const API = 'https://api.plugchoice.com/sdk/v1';
const VERSION = '2026-10-07';

let token: { value: string; expiresAt: number } | null = null;

export async function plugchoiceToken(): Promise<string> {
  if (token && token.expiresAt > Date.now() + 60_000) return token.value;

  const response = await fetch('https://app.plugchoice.com/oauth/token', {
    method: 'POST',
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: process.env.PLUGCHOICE_CLIENT_ID!,
      client_secret: process.env.PLUGCHOICE_CLIENT_SECRET!,
    }),
  });
  if (!response.ok) throw new Error(`Plugchoice token: ${response.status}`);

  const body = await response.json();
  token = { value: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 };
  return token.value;
}

export async function plugchoice(path: string, init: RequestInit = {}) {
  return fetch(`${API}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${await plugchoiceToken()}`,
      'Content-Type': 'application/json',
      'Plugchoice-Version': VERSION,
      ...init.headers,
    },
  });
}
```

## 2. Write your endpoint

Your app asks your server for a client secret whenever Link needs one. It sends the action Link opens for, so your server can scope the client session to exactly that: the charger to work on, or where to add one.

```ts server.ts theme={null}
import { plugchoice } from './plugchoice';

type LinkAction = { action: string; chargerId?: string; siteId?: string };

// What the client secret may reach: only what this action is about.
function scopeFor(action: LinkAction) {
  if (action.action === 'add') {
    // A site of this user's, or a new site at an address Link asks for.
    return action.siteId ? { sites: [action.siteId] } : { new_site: true };
  }
  if (!action.chargerId) throw new Error(`${action.action} needs a charger`);
  return { chargers: [action.chargerId] };
}

app.post('/plugchoice/client-secret', requireSignedInUser, async (req, res) => {
  const response = await plugchoice('/client-sessions', {
    method: 'POST',
    body: JSON.stringify({
      external_user_id: req.user.id,
      scope: scopeFor(req.body),
      language: req.user.language,
    }),
  });
  if (!response.ok) return res.status(502).json(await response.json());

  const session = await response.json();
  res.json({ clientSecret: session.client_secret });
});
```

* `external_user_id` is your own id for the signed-in user. The first client session for it creates that user at Plugchoice.
* The SDK API checks every id in the scope against that user: a charger or site that isn't theirs is [`404 not-found`](/sdk/errors/not-found).
* The client secret works for an hour. Link asks for a new one by itself when it runs out.

<Tip>
  Do you already know your user's address? Send `scope: { location: { reference, address, timezone } }` instead of `new_site`, and Link won't ask for it. See [Client sessions and scopes](/sdk/concepts/client-sessions).
</Tip>

The same request with curl:

```bash theme={null}
curl -X POST https://api.plugchoice.com/sdk/v1/client-sessions \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Plugchoice-Version: 2026-10-07" \
  -H "Content-Type: application/json" \
  -d '{ "external_user_id": "user-123", "scope": { "new_site": true } }'
```

```json theme={null}
{
  "id": "5b0c2f8e-4d1a-4c3b-9e7f-2a6d8c1b0e94",
  "client_secret": "cs_test_Hq3k9d0mR2xYw7pLf8sVb1nC4eTgJ6aZuQ5iKoXyM0w",
  "expires_at": "2026-10-07T13:00:00Z",
  "web_url": "https://connect.plugchoice.com/#cs=cs_test_Hq3k9d0mR2xYw7pLf8sVb1nC4eTgJ6aZuQ5iKoXyM0w"
}
```

## 3. Give the SDK your endpoint

Create one SDK instance when your app starts. Its `fetchClientSecret` calls your endpoint with the action and returns the client secret.

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    import PlugchoiceSDK

    // One instance for your app's lifetime.
    let plugchoice = Plugchoice(fetchClientSecret: { action in
        // action.action ("add", "network", …), action.chargerId, action.siteId
        try await backend.plugchoiceClientSecret(for: action)
    })
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    // One instance for your app's lifetime. The callback runs on the main thread:
    // call your server with a suspending client.
    val plugchoice = Plugchoice(fetchClientSecret = { action ->
        // action.name ("add", "network", …), action.chargerId, action.siteId
        backend.plugchoiceClientSecret(action)
    })
    ```
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    import { configurePlugchoice } from '@plugchoice/react-native';

    // Once, when your app starts.
    configurePlugchoice({
      // action: { action: 'add' | 'network' | …, chargerId?, siteId? }
      fetchClientSecret: (action) => backend.plugchoiceClientSecret(action),
    });
    ```
  </Tab>
</Tabs>

`backend.plugchoiceClientSecret` stands for your own code: it posts the action's name, `chargerId` and `siteId` to your endpoint, with your user's session, and returns `clientSecret`. If it fails or takes longer than 30 seconds, Link shows an error screen with **Try again**.

## 4. Open Link

Open Link with the `add` action, for example from an **Add charger** button:

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    plugchoice.link.present(.addCharger(), from: viewController) { result in
        handle(result)
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    private val openLink = registerForActivityResult(plugchoice.link.contract()) { result ->
        handle(result)
    }

    openLink.launch(LinkAction.addCharger())
    ```
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    import { openLink } from '@plugchoice/react-native';

    const result = await openLink({ action: 'add' });
    handle(result);
    ```
  </Tab>
</Tabs>

Your user agrees to share their charger with you, picks the brand and model, and Link sets the charger up.

<Columns cols={3}>
  <Screen name="consent" alt="Link's consent screen: connect your charger to Acme Energy" caption="Consent, the first time" />

  <Screen name="run-list" alt="Link setting up a charger, step by step" caption="Setting up the charger" />

  <Screen name="done" alt="Link showing that the charger is connected" caption="Done" />
</Columns>

## 5. Handle the result

Link closes with a result. On success, send its `sessionId` to your server.

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    func handle(_ result: LinkResult) {
        switch result.status {
        case .success:
            if let sessionId = result.sessionId {
                Task { try await backend.linkSessionFinished(sessionId) }
            }
        case .cancelled:
            break // Your user left; nothing changed.
        case .error:
            print("Link failed:", result.error?.code ?? "unknown")
        }
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    fun handle(result: LinkResult) {
        when (result.status) {
            LinkResult.Status.SUCCESS -> result.sessionId?.let { backend.linkSessionFinished(it) }
            LinkResult.Status.CANCELLED -> Unit // Your user left; nothing changed.
            LinkResult.Status.ERROR -> Log.w("Link", "Link failed: ${result.error?.code}")
        }
    }
    ```
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    function handle(result: LinkResult) {
      if (result.status === 'success' && result.sessionId) {
        backend.linkSessionFinished(result.sessionId);
      } else if (result.status === 'error') {
        console.warn('Link failed:', result.error?.code);
      }
      // 'cancelled': your user left; nothing changed.
    }
    ```
  </Tab>
</Tabs>

The result also lists the `devices` Link finished, `{ type: "charger", id }`. Use it to update your screen, but let your server decide what's true.

## 6. Confirm on your server

Read the link session with [Get a link session](/sdk/api/get-link-session), check it belongs to the signed-in user, and store its chargers:

```ts server.ts theme={null}
app.post('/plugchoice/link-sessions/:id', requireSignedInUser, async (req, res) => {
  const response = await plugchoice(`/link-sessions/${encodeURIComponent(req.params.id)}`);
  if (!response.ok) return res.sendStatus(404);

  const linkSession = await response.json();
  if (linkSession.external_user_id !== req.user.id) return res.sendStatus(404);

  if (linkSession.status === 'completed') {
    for (const device of linkSession.devices) {
      if (device.type === 'charger') await saveCharger(req.user, device.id);
    }
  }
  res.json({ status: linkSession.status });
});
```

```json theme={null}
{
  "id": "0b8a6f3e-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
  "started_by": "integration",
  "external_user_id": "user-123",
  "site_id": "7d0f6f3e-2b0b-4c55-9b1e-3c6b8f0a9e21",
  "action": "add",
  "device_type": "charger",
  "charger_id": null,
  "status": "completed",
  "charger_ids": ["2f1c8e47-5a4b-4c3d-9e2f-1a0b9c8d7e6f"],
  "devices": [{ "type": "charger", "id": "2f1c8e47-5a4b-4c3d-9e2f-1a0b9c8d7e6f" }],
  "claims": [],
  "abandon_reason": null,
  "error": null,
  "created_at": "2026-10-07T12:00:00Z",
  "opened_at": "2026-10-07T12:00:01Z",
  "ended_at": "2026-10-07T12:06:40Z",
  "expires_at": "2026-10-07T18:00:01Z"
}
```

You can list all of a user's chargers at any time with `GET /sdk/v1/chargers?external_user_id=user-123`. See [Link sessions](/sdk/concepts/link-sessions) for what each status means.

## 7. Offer the next actions

Once a charger is added, your app can open Link for it again: to change its network (`network`), set it up (`setup`) or bring it back online (`reconnect`). Show each only where it works: the charger's `capabilities` from [Get a charger](/sdk/api/get-charger) say what Plugchoice can do for it, and the SDK's `transports()` says what this phone can do. See [Devices and capabilities](/sdk/concepts/capabilities).

<Tabs>
  <Tab title="iOS">
    ```swift theme={null}
    plugchoice.link.present(.network(chargerId: chargerId), from: viewController) { result in
        handle(result)
    }
    ```
  </Tab>

  <Tab title="Android">
    ```kotlin theme={null}
    openLink.launch(LinkAction.network(chargerId))
    ```
  </Tab>

  <Tab title="React Native">
    ```ts theme={null}
    const result = await openLink({ action: 'network', chargerId });
    ```
  </Tab>
</Tabs>

Your endpoint from step 2 already scopes these to the one charger.

## 8. Go live

When your integration works in test mode, email [support@plugchoice.com](mailto:support@plugchoice.com) to go live. Plugchoice switches your integration to live: its client secrets then start with `cs_live_`, and nothing in your code changes. Send your name, logo, accent colour and privacy policy URL too, for the consent screen; see [Consent and branding](/sdk/concepts/consent-and-branding).


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