# Onboard a user in a mobile/native app (get API keys via PKCE)

Let a native or mobile app get a user's own Porkbun API keys **without ever handling their password**. The user signs in — or creates a brand-new Porkbun account — in the system browser, approves once, and control returns to your app with the keys delivered securely out-of-band (PKCE, RFC 7636).

This one flow handles **both** cases automatically: an existing user logs in, a new user taps "Create a New Account" on the same screen. Your app code does not branch on which.

## The flow at a glance

1. App generates a PKCE `codeVerifier` + `codeChallenge`.
2. App calls `POST /apikey/request` with the challenge and its `returnUrl` → gets an `authUrl`.
3. App opens `authUrl` in the **system browser** (ASWebAuthenticationSession / Custom Tabs).
4. User logs in or creates an account, then approves.
5. Browser redirects to your `returnUrl` with `status` + `requestToken`; your app captures it.
6. App calls `POST /apikey/retrieve` with the `requestToken` + `codeVerifier` → receives both keys **once**.

## 1. Generate the PKCE pair

`codeVerifier` is a high-entropy random string you keep in memory. `codeChallenge` is its base64url-unpadded SHA-256.

```swift
// iOS (Swift, CryptoKit)
import CryptoKit
let verifier = Data((0..<32).map { _ in UInt8.random(in: 0...255) }).base64URLEncoded()   // 43 chars
let challenge = Data(SHA256.hash(data: Data(verifier.utf8))).base64URLEncoded()
// base64URLEncoded(): standard base64, then + → -, / → _, strip '='
```

```kotlin
// Android (Kotlin)
val verifier = ByteArray(32).also { SecureRandom().nextBytes(it) }
    .let { Base64.encodeToString(it, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) }
val challenge = MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray())
    .let { Base64.encodeToString(it, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) }
```

## 2. Request (no credentials needed)

```bash
curl -X POST https://api.porkbun.com/api/json/v3/apikey/request \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Acme Domains (iOS)",
    "codeChallenge": "<challenge from step 1>",
    "codeChallengeMethod": "S256",
    "returnUrl": "https://app.acme.example/porkbun/callback"
  }'
```

- `name` — shown to the user on the approval screen. Use your app's name.
- `returnUrl` — **must be an HTTPS URL your app claims as a Universal Link (iOS) / App Link (Android)**. Custom URI schemes (`myapp://`) are **rejected** — another app could register the same scheme and intercept the callback. `returnUrl` requires `codeChallenge`.

Response: `{ "requestToken": "...", "authUrl": "https://porkbun.com/account/apiKeyApproval/<token>", "expiration": "...", "deliveryMode": "pkce" }`. The user has **30 minutes** to approve — enough for a brand-new user to create an account and verify their email in the same session. (After approval, the retrieve step in step 5 has its own 10-minute window.)

## 3. Open `authUrl` in the system browser — not a WebView

Use **`ASWebAuthenticationSession`** (iOS) or **Custom Tabs** (Android). Do **not** use an embedded `WKWebView`/`WebView`: the user must see the real `porkbun.com` address bar (phishing-resistance) and reuse real Porkbun cookies, and an embedded webview could read the password. This is the difference between a secure integration and an insecure one.

```swift
let session = ASWebAuthenticationSession(url: authURL, callbackURLScheme: nil) { callbackURL, error in
    // Universal Link handling also delivers this; see step 4
}
session.presentationContextProvider = self
session.start()
```

On this screen the user either signs in **or taps "Create a New Account"** and completes normal Porkbun signup (email verification, etc.). Either way they land back on the approval screen and approve. You don't handle any of that — it's all in the browser.

## 4. Handle the return redirect

After the decision, Porkbun redirects the browser to your `returnUrl` with query params appended:

```
https://app.acme.example/porkbun/callback?status=approved&requestToken=<token>
```

`status` is `approved` or `denied`. On `approved`, grab the `requestToken` (it matches the one from step 2). **No key is in this URL** — only the token. If `status=denied`, stop.

## 5. Retrieve the keys (once)

```bash
curl -X POST https://api.porkbun.com/api/json/v3/apikey/retrieve \
  -H 'Content-Type: application/json' \
  -d '{"requestToken":"<token>","codeVerifier":"<verifier from step 1>"}'
```

Returns `{ "status":"SUCCESS", "apikey":"pk1_...", "secretapikey":"sk1_..." }`. The **secret is returned exactly once** — persist it immediately in the device keychain/keystore. A later `/retrieve` returns the public key only (`SECRET_ALREADY_CLAIMED`). Retrieve within 10 minutes of approval.

From here, authenticate normal API calls with `apikey` + `secretapikey` (or the `X-API-Key` / `X-Secret-API-Key` headers).

## Why this is safe

- The **password never touches your app** — auth happens on porkbun.com in the system browser.
- The **secret key never rides the redirect or the browser** — it's delivered only to the holder of the `codeVerifier` (your app) via `/apikey/retrieve`. An intercepted redirect leaks nothing usable.
- HTTPS-claimed Universal Links can't be hijacked the way custom URI schemes can.

## Notes & error codes

- The issued key is a **full-account key**. Tell users they can scope it (per-domain / source-IP allowlist) or revoke it anytime at porkbun.com/account/api. Keys created this way are labeled with your app name there.
- `INVALID_INPUT` on `/apikey/request` — `returnUrl` isn't HTTPS, is a custom scheme, or was sent without `codeChallenge`.
- `CODE_VERIFIER_REQUIRED` / `INVALID_CODE_VERIFIER` on `/retrieve` — send the exact `codeVerifier` from step 1.
- `REQUEST_EXPIRED` — the 30-minute approval window, or the 10-minute post-approval retrieve window, lapsed; start over at step 2.
- `SECRET_ALREADY_CLAIMED` — the secret was already returned once; create a new request if it wasn't stored.


---

## More

- Guides (how-tos): https://porkbun.com/llms/guides
- Topic index: https://porkbun.com/llms
- Full reference (one file): https://porkbun.com/llms-full.txt
- OpenAPI spec (full schemas): https://porkbun.com/api/json/v3/spec
- Short overview: https://porkbun.com/llms.txt
- Official MCP server: https://porkbun.com/mcp (`npx -y @porkbunllc/mcp-server`)
- Create API keys: https://porkbun.com/account/api
