---
title: "OAuth Quickstart"
description: "Get credentials, complete the authorization flow, and make your first authenticated API call."
canonical_url: https://developer.onepagecrm.com/oauth/quickstart/
source: Markdown mirror of https://developer.onepagecrm.com/oauth/quickstart/
---

This guide walks through the full Authorization Code + PKCE flow. By the end you will have a working `client_id`, an access token, and a live API call.

**Prerequisites:** Decide whether your app is a [public or confidential client](/oauth/overview/#client-types) before you start. Have your redirect URI ready.

---

## Step 1: Get client credentials

[Request client credentials](/oauth/registration/) from the OnePageCRM
team. You'll need:

- Your application name — users see it on the consent screen
- Your redirect URI(s): the URL OnePageCRM will redirect users to after they approve your app — `https`, or `http://localhost` / `http://127.0.0.1` for development
- Client type: public (browser/mobile app) or confidential (server-side app)

Registration is handled manually and can take a few days — request it
before you plan to build. You'll receive your `client_id` (and
`client_secret` for confidential clients).

---

## Step 2: Generate PKCE values

Before redirecting the user, generate a `code_verifier` and `code_challenge`. These are single-use values that tie the token exchange to this specific login attempt.

**JavaScript**

```javascript
const array = new Uint8Array(64)
crypto.getRandomValues(array)
const codeVerifier = btoa(String.fromCharCode(...array))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')

const encoded = new TextEncoder().encode(codeVerifier)
const digest = await crypto.subtle.digest('SHA-256', encoded)
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')

// Store for use in the callback
sessionStorage.setItem('code_verifier', codeVerifier)
```

**Python**

```python
import secrets
import hashlib
import base64

code_verifier = secrets.token_urlsafe(64)
digest = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode()
```

**Ruby**

```ruby
require 'securerandom'
require 'digest'
require 'base64'

code_verifier = Base64.urlsafe_encode64(SecureRandom.random_bytes(64), padding: false)
code_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
```

**Kotlin**

```kotlin
import java.security.MessageDigest
import java.security.SecureRandom
import android.util.Base64

val bytes = ByteArray(64).also { SecureRandom().nextBytes(it) }
val codeVerifier = Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)

val digest = MessageDigest.getInstance("SHA-256").digest(codeVerifier.toByteArray())
val codeChallenge = Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
```

**Swift**

```swift
import CryptoKit
import Foundation
import Security

var bytes = [UInt8](repeating: 0, count: 64)
_ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
let codeVerifier = Data(bytes).base64EncodedString()
    .replacingOccurrences(of: "+", with: "-")
    .replacingOccurrences(of: "/", with: "_")
    .replacingOccurrences(of: "=", with: "")

let digest = SHA256.hash(data: Data(codeVerifier.utf8))
let codeChallenge = Data(digest).base64EncodedString()
    .replacingOccurrences(of: "+", with: "-")
    .replacingOccurrences(of: "/", with: "_")
    .replacingOccurrences(of: "=", with: "")
```

---

## Step 3: Redirect the user to OnePageCRM

Send the user to the authorization endpoint. On mobile, use the platform's secure browser API, not a WebView.

**JavaScript**

```javascript
const state = crypto.randomUUID()
sessionStorage.setItem('oauth_state', state)

const params = new URLSearchParams({
  response_type: 'code',
  client_id: 'YOUR_CLIENT_ID',
  redirect_uri: 'https://myapp.example.com/callback',
  scope: 'crm.readonly',
  state,
  code_challenge: codeChallenge,
  code_challenge_method: 'S256',
})

window.location.href = `https://secure.onepagecrm.com/oauth/authorize?${params}`
```

**Python**

```python
import secrets
from urllib.parse import urlencode

state = secrets.token_urlsafe(32)
# Store state in session: session['oauth_state'] = state

params = urlencode({
    'response_type': 'code',
    'client_id': 'YOUR_CLIENT_ID',
    'redirect_uri': 'https://myapp.example.com/callback',
    'scope': 'crm.readonly',
    'state': state,
    'code_challenge': code_challenge,
    'code_challenge_method': 'S256',
})

# Flask/Django: redirect to this URL
auth_url = f'https://secure.onepagecrm.com/oauth/authorize?{params}'
```

**Ruby**

```ruby
require 'securerandom'
require 'uri'

state = SecureRandom.urlsafe_base64(32)
# Store in session: session[:oauth_state] = state

params = URI.encode_www_form(
  response_type: 'code',
  client_id: 'YOUR_CLIENT_ID',
  redirect_uri: 'https://myapp.example.com/callback',
  scope: 'crm.readonly',
  state: state,
  code_challenge: code_challenge,
  code_challenge_method: 'S256'
)

# Rails: redirect_to
auth_url = "https://secure.onepagecrm.com/oauth/authorize?#{params}"
```

**Kotlin**

```kotlin
// Uses AppAuth for Android — recommended for secure mobile OAuth
// Add to build.gradle: implementation 'net.openid:appauth:0.11.1'
import android.net.Uri
import net.openid.appauth.*

val serviceConfig = AuthorizationServiceConfiguration(
    Uri.parse("https://secure.onepagecrm.com/oauth/authorize"),
    Uri.parse("https://secure.onepagecrm.com/oauth/token")
)

val authRequest = AuthorizationRequest.Builder(
    serviceConfig,
    "YOUR_CLIENT_ID",
    ResponseTypeValues.CODE,
    // Must be https (Android App Link) — custom schemes like
    // myapp:// are rejected at registration.
    Uri.parse("https://myapp.example.com/oauth/callback")
)
    .setScope("crm.readonly")
    .setCodeVerifier(codeVerifier, codeChallenge, "S256")
    .build()

val authService = AuthorizationService(this)
val intent = authService.getAuthorizationRequestIntent(authRequest)
startActivityForResult(intent, RC_AUTH)
```

**Swift**

```swift
// Uses ASWebAuthenticationSession — Apple's secure OAuth browser
import AuthenticationServices

let state = UUID().uuidString
var components = URLComponents(string: "https://secure.onepagecrm.com/oauth/authorize")!
components.queryItems = [
    .init(name: "response_type", value: "code"),
    .init(name: "client_id",     value: "YOUR_CLIENT_ID"),
    // Must be https (universal link) — custom schemes like
    // myapp:// are rejected at registration.
    .init(name: "redirect_uri",  value: "https://myapp.example.com/oauth/callback"),
    .init(name: "scope",         value: "crm.readonly"),
    .init(name: "state",         value: state),
    .init(name: "code_challenge", value: codeChallenge),
    .init(name: "code_challenge_method", value: "S256"),
]

let session = ASWebAuthenticationSession(
    url: components.url!,
    callback: .https(host: "myapp.example.com", path: "/oauth/callback")  // iOS 17.4+
) { callbackURL, error in
    guard let url = callbackURL, error == nil else { return }
    // Extract code and state from url, then exchange for tokens
}
session.presentationContextProvider = self
session.start()
```

---

## Step 4: Exchange the code for tokens

After the user approves, OnePageCRM redirects back with `?code=...&state=...`. Verify the state, then exchange the code immediately. It expires in **10 minutes** and is single-use.

**curl**

```bash
curl -X POST https://secure.onepagecrm.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=https://myapp.example.com/callback" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "code_verifier=YOUR_CODE_VERIFIER"
```

**JavaScript**

```javascript
const params = new URLSearchParams(window.location.search)

if (params.get('state') !== sessionStorage.getItem('oauth_state')) {
  throw new Error('State mismatch — possible CSRF attack')
}

const codeVerifier = sessionStorage.getItem('code_verifier')
sessionStorage.removeItem('code_verifier')
sessionStorage.removeItem('oauth_state')

const response = await fetch('https://secure.onepagecrm.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: params.get('code'),
    redirect_uri: 'https://myapp.example.com/callback',
    client_id: 'YOUR_CLIENT_ID',
    code_verifier: codeVerifier,
  }),
})
const tokens = await response.json()
```

**Python**

```python
import requests

# Verify state matches what you stored before the redirect
assert request.args['state'] == session['oauth_state'], 'State mismatch'

response = requests.post(
    'https://secure.onepagecrm.com/oauth/token',
    data={
        'grant_type': 'authorization_code',
        'code': request.args['code'],
        'redirect_uri': 'https://myapp.example.com/callback',
        'client_id': 'YOUR_CLIENT_ID',
        'code_verifier': session['code_verifier'],
    }
)
tokens = response.json()
```

**Ruby**

```ruby
require 'net/http'
require 'json'

# Verify state matches what you stored before the redirect
raise 'State mismatch' unless params[:state] == session[:oauth_state]

uri = URI('https://secure.onepagecrm.com/oauth/token')
response = Net::HTTP.post_form(uri, {
  grant_type: 'authorization_code',
  code: params[:code],
  redirect_uri: 'https://myapp.example.com/callback',
  client_id: 'YOUR_CLIENT_ID',
  code_verifier: session[:code_verifier]
})
tokens = JSON.parse(response.body)
```

**Kotlin**

```kotlin
import android.net.Uri
import net.openid.appauth.*
import okhttp3.*

// AppAuth handles the callback and code exchange
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == RC_AUTH) {
        val resp = AuthorizationResponse.fromIntent(data!!)
        val ex = AuthorizationException.fromIntent(data)

        if (resp != null) {
            val authService = AuthorizationService(this)
            authService.performTokenRequest(resp.createTokenExchangeRequest()) { tokenResp, ex ->
                if (tokenResp != null) {
                    val accessToken = tokenResp.accessToken
                    val refreshToken = tokenResp.refreshToken
                }
            }
        }
    }
}
```

**Swift**

```swift
import Foundation

var request = URLRequest(url: URL(string: "https://secure.onepagecrm.com/oauth/token")!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

let body = [
    "grant_type=authorization_code",
    "code=\(authCode)",
    "redirect_uri=https://myapp.example.com/oauth/callback",
    "client_id=YOUR_CLIENT_ID",
    "code_verifier=\(codeVerifier)",
].joined(separator: "&")
request.httpBody = body.data(using: .utf8)

let (data, _) = try await URLSession.shared.data(for: request)
let tokens = try JSONDecoder().decode(TokenResponse.self, from: data)
```

You receive:

```json
{
  "access_token": "k2tV5VE1b3...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "crm.readonly",
  "aud": "https://app.onepagecrm.com",
  "refresh_token": "dGhpcyBpcyBh..."
}
```

The `aud` field is the CRM API base URL for this user's account — it may be a regional host rather than `app.onepagecrm.com`. Always build API URLs from `aud` instead of hard-coding a host. Store `access_token` in memory and `refresh_token` securely.

---

## Step 5: Make an API request

**curl**

```bash
# Base URL comes from the token response's aud field
curl https://app.onepagecrm.com/api/v3/contacts.json \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

**JavaScript**

```javascript
const response = await fetch(`${audience}/api/v3/contacts.json`, {
  headers: { Authorization: `Bearer ${accessToken}` }
})
const data = await response.json()
```

**Python**

```python
import requests

response = requests.get(
    f'{audience}/api/v3/contacts.json',
    headers={'Authorization': f'Bearer {access_token}'}
)
data = response.json()
```

**Ruby**

```ruby
require 'net/http'
require 'json'

uri = URI("#{audience}/api/v3/contacts.json")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{access_token}"

response = http.request(request)
data = JSON.parse(response.body)
```

**Kotlin**

```kotlin
import okhttp3.*

val request = Request.Builder()
    .url("$audience/api/v3/contacts.json")
    .header("Authorization", "Bearer $accessToken")
    .build()

val response = OkHttpClient().newCall(request).execute()
val data = response.body!!.string()
```

**Swift**

```swift
var request = URLRequest(url: URL(string: "\(audience)/api/v3/contacts.json")!)
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
let contacts = try JSONDecoder().decode(ContactsResponse.self, from: data)
```

---

## Step 6: Renew the access token

Access tokens expire after 60 minutes. Use the refresh token to get a new one silently, with no user interaction required.

**curl**

```bash
curl -X POST https://secure.onepagecrm.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=YOUR_REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID"
```

**JavaScript**

```javascript
const response = await fetch('https://secure.onepagecrm.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: storedRefreshToken,
    client_id: 'YOUR_CLIENT_ID',
  }),
})
const tokens = await response.json()
// Always save the new refresh token — the old one is now invalid
storedRefreshToken = tokens.refresh_token
```

**Python**

```python
response = requests.post(
    'https://secure.onepagecrm.com/oauth/token',
    data={
        'grant_type': 'refresh_token',
        'refresh_token': stored_refresh_token,
        'client_id': 'YOUR_CLIENT_ID',
    }
)
tokens = response.json()
stored_refresh_token = tokens['refresh_token']  # always save the new one
```

**Ruby**

```ruby
response = Net::HTTP.post_form(
  URI('https://secure.onepagecrm.com/oauth/token'),
  grant_type: 'refresh_token',
  refresh_token: stored_refresh_token,
  client_id: 'YOUR_CLIENT_ID'
)
tokens = JSON.parse(response.body)
stored_refresh_token = tokens['refresh_token']  # always save the new one
```

**Kotlin**

```kotlin
val body = FormBody.Builder()
    .add("grant_type", "refresh_token")
    .add("refresh_token", storedRefreshToken)
    .add("client_id", "YOUR_CLIENT_ID")
    .build()

val request = Request.Builder()
    .url("https://secure.onepagecrm.com/oauth/token")
    .post(body)
    .build()

val response = OkHttpClient().newCall(request).execute()
val tokens = JSONObject(response.body!!.string())
storedRefreshToken = tokens.getString("refresh_token")  // always save the new one
```

**Swift**

```swift
var request = URLRequest(url: URL(string: "https://secure.onepagecrm.com/oauth/token")!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpBody = "grant_type=refresh_token&refresh_token=\(storedRefreshToken)&client_id=YOUR_CLIENT_ID"
    .data(using: .utf8)

let (data, _) = try await URLSession.shared.data(for: request)
let tokens = try JSONDecoder().decode(TokenResponse.self, from: data)
storedRefreshToken = tokens.refreshToken  // always save the new one
```

> **Public clients:** Every refresh response includes a new refresh token. Always replace the old one. Reusing a rotated token revokes the entire session.

---

## Next steps

- [Reference](/oauth/reference/): every endpoint, parameter, error code, and security detail.
