> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer-stage.shipbob.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer-stage.shipbob.dev/_mcp/server.

# Migrate to PKCE

> Add PKCE (Proof Key for Code Exchange) to an existing OAuth app's authorization flow.

PKCE (Proof Key for Code Exchange) adds a one-time secret to the OAuth authorization code exchange, protecting against intercepted authorization codes. It is now part of ShipBob's OAuth flow. This guide walks through adding it to an OAuth app that was built before PKCE.

## What changes

PKCE adds two parameters to the authorize request and one to the token exchange. Nothing else about your OAuth flow changes - the `client_id`, `client_secret`, `redirect_uri`, and scopes all stay the same, and the `client_secret` is still required.

| Step              | Before                                    | After                                                |
| ----------------- | ----------------------------------------- | ---------------------------------------------------- |
| Authorize request | `client_id`, `redirect_uri`, ...          | adds `code_challenge` + `code_challenge_method=S256` |
| Token exchange    | `client_id`, `client_secret`, `code`, ... | adds `code_verifier`                                 |

## Migration steps

For each authorization request, generate a fresh PKCE pair:

* **`code_verifier`** – a cryptographically random string, 43-128 characters, using only the URL-safe characters `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, `~`. Keep it secret and store it for the token exchange (Step 3).
* **`code_challenge`** – the Base64-URL-encoded (no padding) SHA-256 hash of the `code_verifier`.

Always use the `S256` challenge method. Never use the `plain` method in production.

```javascript
import crypto from "crypto";

function base64UrlEncode(buffer) {
  return buffer
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

// 32 random bytes -> 43-character verifier
const codeVerifier = base64UrlEncode(crypto.randomBytes(32));

const codeChallenge = base64UrlEncode(
  crypto.createHash("sha256").update(codeVerifier).digest()
);

console.log({ codeVerifier, codeChallenge });
```

```python
import base64
import hashlib
import os

def base64_url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")

# 32 random bytes -> 43-character verifier
code_verifier = base64_url_encode(os.urandom(32))

code_challenge = base64_url_encode(
    hashlib.sha256(code_verifier.encode("ascii")).digest()
)

print(code_verifier, code_challenge)
```

```powershell
$bytes = New-Object byte[] 32
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
$codeVerifier = [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+','-').Replace('/','_')

$sha256 = [System.Security.Cryptography.SHA256]::Create()
$hashBytes = $sha256.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($codeVerifier))
$codeChallenge = [Convert]::ToBase64String($hashBytes).TrimEnd('=').Replace('+','-').Replace('/','_')

"$codeVerifier`n$codeChallenge"
```

Append `code_challenge` and `code_challenge_method=S256` to the authorize URL you already build:

```
https://auth.shipbob.com/connect/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fwww.myapp.com%2Fintegrate%2Fshipbob%2Fcallback
  &response_type=code
  &scope=openid%20offline_access%20channels_read
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
```

Include the matching `code_verifier` in the token request. The `client_secret` is still required - PKCE is an additional layer, not a replacement.

```bash
curl -X POST "https://auth.shipbob.com/connect/token" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "client_id=YOUR_CLIENT_ID" \
  --data-urlencode "client_secret=YOUR_CLIENT_SECRET" \
  --data-urlencode "redirect_uri=https://www.myapp.com/integrate/shipbob/callback" \
  --data-urlencode "code=AUTHORIZATION_CODE" \
  --data-urlencode "code_verifier=code_verifier_from_step_1"
```

The `code_verifier` must match the `code_challenge` you sent on the authorize request in Step 2. Generate a new pair for every authorization - never reuse a `code_verifier`.

PKCE applies only to the initial authorization code exchange. You do **not** need to send a `code_verifier` or `code_challenge` when refreshing tokens.

For the full OAuth walkthrough, see the [Authentication](/auth) guide.