---
title: Device Authorization Guide (RFC 8628)
audience: public
summary: Obtain WeeConnectPay OAuth2 client credentials through the RFC 8628 device flow — the recommended plugin onboarding path, with endpoints, polling rules, and worked examples.
---

# WeeConnectPay API - Device Authorization Guide (RFC 8628)

This guide explains how to use the Device Authorization flow to obtain OAuth2 client credentials without requiring the merchant to manually copy-paste secrets. This is the recommended approach for plugin onboarding.

> **Branded version:** Open [DEVICE_AUTHORIZATION_IMPLEMENTATION_GUIDE.html](DEVICE_AUTHORIZATION_IMPLEMENTATION_GUIDE.html) for a styled, printable version of this guide with flow diagrams and visual code displays.
>
> **Security analysis:** See [DEVICE_AUTHORIZATION_SECURITY.md](DEVICE_AUTHORIZATION_SECURITY.md) for the full threat model, security architecture, and compliance details.
>
> **See also:** The [OAuth2 Integration Guide](OAUTH_INTEGRATION_GUIDE.md) explains how to use the client credentials once obtained. The [Integration Architecture Guide](INTEGRATION_ARCHITECTURE_GUIDE.md) explains what your token represents and how Clover entities relate.

## Environments

| Environment | Authorization Page | API Base URL |
|-------------|-------------------|--------------|
| **Sandbox** | `https://login.sandbox.weeconnectpay.com/connect` | `https://apidev.weeconnectpay.com` |
| **Staging** | `https://login.staging.weeconnectpay.com/connect` | `https://apidev.weeconnectpay.com` |
| **Production** | `https://login.weeconnectpay.com/connect` | `https://api.weeconnectpay.com` |

> **Important:** Device codes are environment-specific. A code generated against a given environment's API must be approved on that environment's authorization page. Do not mix credentials across environments.
>
> **Sandbox vs Staging:** The sandbox environment is for merchants and integrators who want to test the integration — access is granted by employee invite to a pre-configured sandbox merchant. The staging environment is for developers actively building integrations and offers a stable environment unaffected by other testers. Both share the same API base URL. For day-to-day development, use **staging**; for demonstrating or evaluating the product, use **sandbox**.

---

## Overview

The Device Authorization Grant (RFC 8628) lets a plugin obtain credentials without the merchant ever seeing a client secret. Instead:

1. Your plugin sends its platform identity and requests a short, human-readable code from the API
2. The merchant visits the WeeConnectPay login page, authenticates via Clover OAuth, and enters the code
3. The merchant reviews and approves the connection
4. Your plugin automatically receives client credentials

This is the same pattern used by GitHub CLI (`gh auth login`), Microsoft Azure CLI, and Google smart TV authentication.

### Mental Model: Device = Integration

In RFC 8628, the "device" is the thing being authorized. In WeeConnectPay, **the "device" is the integration** — a specific plugin instance on a specific store. Every device authorization request carries `platform_data` that identifies the plugin instance, and the API uses this to create or link to an Integration record before the merchant even sees the approval page.

This means:
- Every device code is associated with an Integration from the moment it is created
- The merchant's approval page shows the pre-linked integration — there is no option to choose or change it
- There is no way to obtain credentials without an integration; the two are always paired

### Multi-Merchant / Franchise Scenario

The device flow supports connecting multiple websites to multiple Clover merchants. This is common in franchise operations where each location has its own Clover merchant account and its own website (e.g., subdomain per franchise).

**How it works:**

1. Each franchise website requests its own device code (same `app_uuid`, different `platform_data.domain` and `platform_data.wordpress.site_url`)
2. Each device code creates a separate Integration record (because the `site_url` + `db_prefix` combination is unique)
3. The franchise owner enters each code on the WeeConnectPay authorization page, **logging in via Clover OAuth** as the correct Clover merchant before clicking "Verify Code"
4. Each approval links the Integration to the authenticated merchant's Clover context

**Important:** The code entry page deliberately does **not** auto-submit when all characters are filled. This gives the merchant time to:
- Verify they are authenticated as the correct Clover merchant
- Authorize as a different merchant if needed (via "Authorize as different merchant", which triggers a new Clover OAuth login)

This pause is critical for multi-merchant setups where the same person manages multiple Clover accounts.

**Example flow for a 3-location franchise:**

```
Location A (shop-a.example.com) → Code: WDJK-MJHT → Merchant: "Coffee Shop Downtown"
Location B (shop-b.example.com) → Code: XFGN-4R7C → Merchant: "Coffee Shop Uptown"
Location C (shop-c.example.com) → Code: HTPQ-2VKE → Merchant: "Coffee Shop Airport"
```

The franchise owner enters each code, logging in via Clover OAuth as the corresponding merchant, and approves. Each website gets credentials tied to its specific Clover merchant.

```
┌─────────────────┐                              ┌─────────────────┐
│                 │  1. POST /v1/connect/authorize │                 │
│  Your Plugin    │ ───────────────────────────> │  WeeConnectPay  │
│                 │  2. user_code + device_code  │  API            │
│                 │ <─────────────────────────── │                 │
│                 │                              │                 │
│  Displays code  │                              │                 │
│  "WDJB-MJHT"   │                              │                 │
│  to merchant    │                              │                 │
│                 │                              │                 │
│                 │  3. POST /v1/connect/token    │                 │
│                 │  (polls every 5 seconds)     │                 │
│                 │ ───────────────────────────> │                 │
│                 │                              │                 │
│                 │  4. client_id + secret       │  (after merchant│
│                 │ <─────────────────────────── │   approves)     │
└─────────────────┘                              └─────────────────┘

                    ┌─────────────────┐
                    │  Merchant opens  │
                    │  login.weecon..  │
                    │  nectpay.com     │
                    │  /connect        │
                    │                  │
                    │  Enters code     │
                    │  "WDJB-MJHT"     │
                    │  Clicks Approve  │
                    └─────────────────┘
```

---

## Quick Start

### 1. Request a Device Code

Your plugin calls this endpoint with no authentication required:

```bash
curl -X POST https://apidev.weeconnectpay.com/v1/connect/authorize \
  -H "Content-Type: application/json" \
  -d '{
    "app_uuid": "ZAVE6HARYPJ6P",
    "platform_key": "woocommerce",
    "client_name": "My WooCommerce Store",
    "platform_data": {
      "domain": "mystore.example.com",
      "woocommerce_version": "9.6.2",
      "wordpress": {
        "site_url": "https://mystore.example.com",
        "home_url": "https://mystore.example.com",
        "db_prefix": "wp_",
        "version": "6.7.2",
        "plugins_url": "https://mystore.example.com/wp-content/plugins",
        "settings_url": "https://mystore.example.com/wp-admin/admin.php?page=wc-settings&tab=checkout&section=weeconnectpay"
      }
    },
    "metadata": {
      "integration_version": "3.2.1"
    }
  }'
```

**Response:**
```json
{
  "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk6eS...",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://login.weeconnectpay.com/connect",
  "verification_uri_complete": "https://login.weeconnectpay.com/connect?code=WDJB-MJHT",
  "expires_in": 900,
  "interval": 5
}
```

### 2. Display the Code to the Merchant

Show the `user_code` prominently in your plugin's admin UI, along with a link to `verification_uri_complete`:

```
╔══════════════════════════════════════════════╗
║  Connect to Clover via WeeConnectPay        ║
║                                              ║
║  Enter this code on the WeeConnectPay        ║
║  login page to connect your store:           ║
║                                              ║
║           WDJB - MJHT                        ║
║                                              ║
║  [Open Login Page] ← link to verification_uri ║
║                                              ║
║  Code expires in 15 minutes.                 ║
╚══════════════════════════════════════════════╝
```

> **Tip:** Use `verification_uri_complete` for the link — it pre-fills the code on the authorization page so the merchant doesn't have to type it.

### 3. Poll for Credentials

While the merchant is approving, your plugin polls every `interval` seconds:

```bash
curl -X POST https://apidev.weeconnectpay.com/v1/connect/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
    "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk6eS..."
  }'
```

**While waiting (400):**
```json
{
  "error": "authorization_pending",
  "error_description": "The user hasn't entered the code yet."
}
```

**On approval (200):**
```json
{
  "client_id": "9a1b2c3d-4e5f-6789-abcd-ef0123456789",
  "client_secret": "your-client-secret-here",
  "token_type": "client_credentials",
  "message": "Device authorized successfully. Use these credentials with the client_credentials grant."
}
```

### 4. Store and Use Credentials

Save the `client_id` and `client_secret` securely. These are standard OAuth2 client credentials — use them exactly as described in the [OAuth2 Integration Guide](OAUTH_INTEGRATION_GUIDE.md):

```bash
curl -X POST https://apidev.weeconnectpay.com/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "9a1b2c3d-4e5f-6789-abcd-ef0123456789",
    "client_secret": "your-client-secret-here"
  }'
```

---

## API Reference

### Request Device Code — `POST /v1/connect/authorize`

No authentication required. Called by the plugin to start the device flow.

**Headers:**
| Header | Value |
|--------|-------|
| `Content-Type` | `application/json` |

**Body Parameters:**
| Parameter | Required | Description |
|-----------|----------|-------------|
| `app_uuid` | Yes | The Clover App UUID (provided by WeeConnectPay) |
| `platform_key` | Yes | Platform identifier: `woocommerce`, `zoho`, or `prestashop` |
| `platform_data` | Yes | Platform identity object (see below). Identifies the integration instance. Fields vary by platform. Used to create or link an Integration. |
| `client_name` | No | Friendly name for the connection (e.g., store name) |
| `metadata` | No | Object with optional fields: `integration_version`, `platform_version` |

**`platform_data` fields (WooCommerce):**
| Field | Required | Description |
|-------|----------|-------------|
| `domain` | Yes | The store's domain (e.g., `mystore.example.com`) |
| `woocommerce_version` | Yes | WooCommerce version (e.g., `9.6.2`) |
| `wordpress.site_url` | Yes | WordPress `site_url()` |
| `wordpress.home_url` | Yes | WordPress `home_url()` |
| `wordpress.db_prefix` | Yes | WordPress database prefix (e.g., `wp_`) |
| `wordpress.version` | Yes | WordPress version (e.g., `6.7.2`) |
| `wordpress.plugins_url` | Yes | WordPress plugins URL |
| `wordpress.settings_url` | Yes | WeeConnectPay settings page URL in wp-admin |

**`platform_data` fields (Zoho):**
| Field | Required | Description |
|-------|----------|-------------|
| `org_id` | Yes | The Zoho Organization ID |

> **Note:** The `platform_data` and `metadata` fields vary by platform. The tables above document the known fields for WooCommerce and Zoho. Integrators should communicate to WeeConnectPay what contextual data is available from their platform that could help identify their session or integration instance — additional fields may be added as new platforms are onboarded.

**Response (200):**
| Field | Type | Description |
|-------|------|-------------|
| `device_code` | string | High-entropy code for polling (keep secret, do not show to user) |
| `user_code` | string | Short code for the merchant to enter (format: `XXXX-XXXX`) |
| `verification_uri` | string | URL where the merchant enters the code |
| `verification_uri_complete` | string | URL with the code pre-filled as a query parameter |
| `expires_in` | integer | Seconds until the codes expire (default: 900 = 15 minutes) |
| `interval` | integer | Minimum seconds between polling requests (default: 5) |

**Rate limit:** 10 requests per minute per IP.

---

### Poll for Credentials — `POST /v1/connect/token`

No authentication required. Called by the plugin to check if the merchant has approved.

**Headers:**
| Header | Value |
|--------|-------|
| `Content-Type` | `application/json` |

**Body Parameters:**
| Parameter | Required | Description |
|-----------|----------|-------------|
| `grant_type` | Yes | Must be `urn:ietf:params:oauth:grant-type:device_code` |
| `device_code` | Yes | The `device_code` from the authorization response |

**Success Response (200):**
| Field | Type | Description |
|-------|------|-------------|
| `client_id` | string | OAuth client ID (UUID format) |
| `client_secret` | string | OAuth client secret (shown once, store immediately) |
| `token_type` | string | Always `client_credentials` |
| `message` | string | Human-readable success message |

> **Critical:** The `client_secret` is delivered **exactly once**. Passport hashes the secret before database storage — the plaintext is never persisted. The temporary encrypted copy held during the device flow is cleared after delivery. If you fail to store it, the merchant must start the device flow again.

**Error Responses (400):**

| Error Code | Description | Action |
|------------|-------------|--------|
| `authorization_pending` | Merchant hasn't entered the code yet | Continue polling at the specified interval |
| `slow_down` | Polling too fast | Increase polling interval by the amount in `interval` |
| `expired_token` | Code expired (15 minutes elapsed) | Start over with a new device code request |
| `access_denied` | Merchant denied the request | Show an error and let the merchant retry |
| `invalid_grant` | Device code is invalid or already consumed | Start over with a new device code request |

**Rate limit:** 30 requests per minute per IP + per-device-code interval enforcement.

---

## Polling Best Practices

Your polling implementation must respect the server's `interval` and handle all error responses:

```javascript
async function pollForCredentials(deviceCode, interval = 5) {
  const maxAttempts = 180; // 15 minutes at 5-second intervals
  let attempts = 0;
  let currentInterval = interval;

  while (attempts < maxAttempts) {
    await sleep(currentInterval * 1000);
    attempts++;

    const response = await fetch('https://apidev.weeconnectpay.com/v1/connect/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
        device_code: deviceCode,
      }),
    });

    const data = await response.json();

    if (response.ok) {
      // Success! Store credentials securely.
      return { clientId: data.client_id, clientSecret: data.client_secret };
    }

    switch (data.error) {
      case 'authorization_pending':
        // Keep polling
        break;
      case 'slow_down':
        currentInterval = data.interval; // Server increased the interval
        break;
      case 'expired_token':
        throw new Error('Code expired. Please request a new code.');
      case 'access_denied':
        throw new Error('The merchant denied the authorization request.');
      default:
        throw new Error(`Unexpected error: ${data.error}`);
    }
  }

  throw new Error('Polling timed out.');
}
```

### Rules

1. **Always wait** the full `interval` seconds before each poll
2. **Respect `slow_down`** — update your interval to the value in the response
3. **Stop on terminal errors** — `expired_token`, `access_denied`, and `invalid_grant` are final
4. **Set a maximum timeout** — don't poll forever (15 minutes matches the code expiry)

---

## Code Examples

### PHP (WordPress / PrestaShop)

```php
<?php

$apiBaseUrl = 'https://apidev.weeconnectpay.com';

// Step 1: Request device code
function requestDeviceCode($apiBaseUrl, $appUuid, $platformKey, $clientName = null) {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => "{$apiBaseUrl}/v1/connect/authorize",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode(array_filter([
            'app_uuid' => $appUuid,
            'platform_key' => $platformKey,
            'client_name' => $clientName,
            'platform_data' => [
                'domain' => parse_url(get_site_url(), PHP_URL_HOST),
                'woocommerce_version' => WC()->version,
                'wordpress' => [
                    'site_url' => get_site_url(),
                    'home_url' => get_home_url(),
                    'db_prefix' => $GLOBALS['wpdb']->prefix,
                    'version' => get_bloginfo('version'),
                    'plugins_url' => plugins_url(),
                    'settings_url' => admin_url('admin.php?page=wc-settings&tab=checkout&section=weeconnectpay'),
                ],
            ],
            'metadata' => [
                'integration_version' => WEECONNECTPAY_VERSION,
            ],
        ])),
    ]);
    $response = curl_exec($ch);
    curl_close($ch);
    return json_decode($response, true);
}

// Step 2: Poll for credentials
function pollForCredentials($apiBaseUrl, $deviceCode, $interval = 5) {
    $maxAttempts = 180;
    $attempts = 0;

    while ($attempts < $maxAttempts) {
        sleep($interval);
        $attempts++;

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => "{$apiBaseUrl}/v1/connect/token",
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_POSTFIELDS => json_encode([
                'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
                'device_code' => $deviceCode,
            ]),
        ]);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        $data = json_decode($response, true);

        if ($httpCode === 200) {
            return $data; // { client_id, client_secret, token_type, message }
        }

        if ($data['error'] === 'slow_down') {
            $interval = $data['interval'];
        } elseif (in_array($data['error'], ['expired_token', 'access_denied', 'invalid_grant'])) {
            throw new Exception("Device flow failed: {$data['error']}");
        }
        // authorization_pending: continue polling
    }

    throw new Exception('Device flow timed out.');
}
```

### Python (Zoho Catalyst)

```python
import requests
import time

API_BASE_URL = 'https://apidev.weeconnectpay.com'

def request_device_code(app_uuid, platform_key, client_name=None, platform_data=None):
    response = requests.post(
        f'{API_BASE_URL}/v1/connect/authorize',
        json={
            'app_uuid': app_uuid,
            'platform_key': platform_key,
            'client_name': client_name,
            'platform_data': platform_data,
        }
    )
    response.raise_for_status()
    return response.json()

def poll_for_credentials(device_code, interval=5):
    max_attempts = 180
    for _ in range(max_attempts):
        time.sleep(interval)

        response = requests.post(
            f'{API_BASE_URL}/v1/connect/token',
            json={
                'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
                'device_code': device_code,
            }
        )

        data = response.json()

        if response.status_code == 200:
            return data  # { client_id, client_secret, token_type, message }

        if data['error'] == 'slow_down':
            interval = data['interval']
        elif data['error'] in ('expired_token', 'access_denied', 'invalid_grant'):
            raise Exception(f"Device flow failed: {data['error']}")
        # authorization_pending: continue polling

    raise TimeoutError('Device flow timed out.')

# Usage
result = request_device_code('ZAVE6HARYPJ6P', 'zoho', 'My Zoho App', platform_data={'org_id': '12345678'})
print(f"Enter code {result['user_code']} at {result['verification_uri']}")
credentials = poll_for_credentials(result['device_code'], result['interval'])
print(f"Connected! Client ID: {credentials['client_id']}")
```

---

## User Code Format

- **Format:** `XXXX-XXXX` (8 characters with a hyphen separator)
- **Character set:** Letters `A C D E F G H J K M N P Q R T U V W X Y Z` and digits `2 3 4 6 7 9`
- **Excluded characters:** `0` / `O`, `1` / `I` / `L`, `5` / `S`, `8` / `B` (to avoid confusion when typing)
- **Case-insensitive:** The API accepts the code in any case, with or without the hyphen

---

## Security Notes

1. **`device_code` is secret** — never display it to the user. Only `user_code` is shown.
2. **Credentials are one-time** — the `client_secret` is delivered exactly once. Passport only stores a hash — the plaintext was never persisted. The temporary encrypted copy from the device flow is cleared after delivery.
3. **Codes expire in 15 minutes** — if the merchant doesn't approve in time, start over.
4. **Rate limiting is enforced** — respect the `interval` and `slow_down` responses.
5. **Clover OAuth login required** — the merchant must authenticate via Clover OAuth on the authorization page to approve.
6. **IP logging** — both the requesting plugin's IP and the approving merchant's IP are logged for audit.

---

## Comparison: Device Flow vs. Manual Credentials

| Aspect | Device Authorization Flow | Manual Client Credentials |
|--------|--------------------------|--------------------------|
| **Setup effort** | Enter a code, click Approve | Copy-paste client_id and secret |
| **Secret exposure** | Never visible in browser | Displayed once in dashboard |
| **Error-prone** | No — automatic delivery | Yes — copy-paste mistakes |
| **Non-technical users** | Simple and clear | Confusing |
| **Integration linking** | Automatic — integration created from `platform_data` at authorize time | Manual — must create integration separately |
| **Plugin implementation** | Slightly more complex | Simple config fields |

> **Recommendation:** Use the Device Authorization flow for all new integrations. Every device authorization creates or links to an Integration automatically.

---

## Troubleshooting

| Problem | Solution |
|---------|----------|
| `invalid_platform` error on authorize | The `platform_key` doesn't match the app. Check with WeeConnectPay for valid keys. |
| `platform_data` validation error | `platform_data` is required. Required fields vary by platform — for WooCommerce, ensure `domain`, `woocommerce_version`, and `wordpress` sub-fields are all present; for Zoho, ensure `org_id` is present. |
| Code expired before merchant approved | The merchant took longer than 15 minutes. Request a new code. |
| `access_denied` on poll | The merchant clicked "Deny" on the authorization page. Ask them to retry. |
| `slow_down` responses | Your plugin is polling too fast. Use the `interval` from the response. |
| Clover OAuth login fails | The merchant could not authenticate with Clover. Ask them to verify their Clover credentials and try again. |
| `invalid_grant` immediately | The device code is wrong or was already consumed. Request a new code. |

---

## Support

For additional help with the Device Authorization flow:

1. Verify you're using the correct `app_uuid` for your environment
2. Ensure the `platform_key` is valid for your app (`woocommerce`, `zoho`, or `prestashop`)
3. Check that the merchant is using the correct environment's authorization page
4. See the [OAuth2 Integration Guide](OAUTH_INTEGRATION_GUIDE.md) for using credentials after obtaining them

For additional support, contact the WeeConnectPay team.
