|
| 1 | +# Coinbase Commerce to Business Migration - Backend Review Checklist |
| 2 | + |
| 3 | +**Migration Deadline: March 31, 2026** |
| 4 | +**Reference**: [Coinbase Transition Guide](https://help.coinbase.com/en/transitioning-from-coinbase-commerce-to-coinbase-business) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## 1. Coinbase Dashboard / Account Setup |
| 9 | + |
| 10 | +- [ ] **Create Coinbase Business account** (or convert existing Commerce account) |
| 11 | + - [Getting Started](https://docs.cdp.coinbase.com/coinbase-business/introduction/get-started) |
| 12 | +- [ ] **Complete KYB (Know Your Business) verification** if not already done |
| 13 | +- [ ] **Generate CDP Secret API Key** in the [CDP Portal](https://portal.cdp.coinbase.com/projects/api-keys) |
| 14 | + - Go to the **Secret API Keys** tab and click **Create API key** |
| 15 | + - Signature algorithm: Ed25519 (recommended) or ECDSA |
| 16 | + - Save the **Key ID** (UUID) → `CDP_API_KEY_ID` |
| 17 | + - Save the **Key Secret** (base64 string) → `CDP_API_KEY_SECRET` |
| 18 | + - These are shown only once |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## 2. Environment Variables to Configure |
| 23 | + |
| 24 | +| Variable | Description | Where | |
| 25 | +|---|---|---| |
| 26 | +| `CDP_API_KEY_ID` | Secret API Key ID (UUID) from [CDP Portal](https://portal.cdp.coinbase.com/projects/api-keys) | All environments | |
| 27 | +| `CDP_API_KEY_SECRET` | Secret API Key secret (base64) from CDP Portal | All environments (secrets manager) | |
| 28 | +| `CDP_SANDBOX` | `true` for sandbox (no real transactions), `false` for production | Per environment | |
| 29 | +| `COINBASE_PAYMENT_LINK_WEBHOOK_SECRET` | From webhook subscription metadata | All environments | |
| 30 | + |
| 31 | +### Variables to Keep (Legacy - Transition Period) |
| 32 | + |
| 33 | +| Variable | Reason | |
| 34 | +|---|---| |
| 35 | +| `COINBASE_COMMERCE_WEBHOOK_SECRET` | Legacy Commerce API — **kept during transition period** for backward compatibility | |
| 36 | + |
| 37 | +> **Note:** Legacy Commerce variables will be removed after migration is confirmed complete and all in-flight Commerce charges have settled. |
| 38 | +
|
| 39 | +--- |
| 40 | + |
| 41 | +## 3. Webhook Configuration |
| 42 | + |
| 43 | +- [ ] **Register webhook endpoint** in Coinbase Business dashboard |
| 44 | + - URL: `https://<api-domain>/api/v1/webhooks/coinbase` |
| 45 | + - Content-Type: `application/json` |
| 46 | +- [ ] **Subscribe to events**: |
| 47 | + - `payment_link.payment.success` |
| 48 | + - `payment_link.payment.failed` |
| 49 | + - `payment_link.payment.expired` |
| 50 | +- [ ] **Save the webhook secret** from the subscription metadata response |
| 51 | + - Set as `COINBASE_PAYMENT_LINK_WEBHOOK_SECRET` env var |
| 52 | +- [ ] **Test webhook delivery** using Coinbase's test tools or [Postman collection](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/postman-files) |
| 53 | + |
| 54 | +### Webhook Signature Format (new) |
| 55 | +- Header: `X-Hook0-Signature` |
| 56 | +- Format: `t=<timestamp>,h=<header_names>,v1=<hmac_sha256>` |
| 57 | +- Replay protection: Rejects events older than 5 minutes |
| 58 | + |
| 59 | +### Old Format (still supported during transition) |
| 60 | +- Header: `X-CC-Webhook-Signature` — legacy Commerce format |
| 61 | +- The webhook endpoint auto-detects the format based on which signature header is present |
| 62 | +- Both formats are supported simultaneously via `_detect_webhook_format()` |
| 63 | + |
| 64 | +--- |
| 65 | + |
| 66 | +## 4. API Authentication Changes |
| 67 | + |
| 68 | +### Old (Commerce) |
| 69 | +``` |
| 70 | +X-CC-Api-Key: <api_key> |
| 71 | +X-CC-Version: 2018-03-22 |
| 72 | +``` |
| 73 | + |
| 74 | +### New (CDP / Business) |
| 75 | +``` |
| 76 | +Authorization: Bearer <signed_JWT> |
| 77 | +Content-Type: application/json |
| 78 | +``` |
| 79 | + |
| 80 | +JWT generation is handled by the `cdp-sdk` Python package using: |
| 81 | +- `CDP_API_KEY_ID` (UUID) and `CDP_API_KEY_SECRET` (base64) |
| 82 | +- Supports both Ed25519 and ECDSA key types (SDK auto-detects) |
| 83 | +- See: https://docs.cdp.coinbase.com/api-reference/v2/authentication |
| 84 | + |
| 85 | +Implementation: `src/services/coinbase_auth.py` |
| 86 | + |
| 87 | +--- |
| 88 | + |
| 89 | +## 5. Payment Link API Endpoints |
| 90 | + |
| 91 | +### Coinbase Business API (upstream) |
| 92 | + |
| 93 | +Base URL: `https://business.coinbase.com` |
| 94 | + |
| 95 | +| Operation | Method | Production Path | Sandbox Path | |
| 96 | +|---|---|---|---| |
| 97 | +| Create | `POST` | `/api/v1/payment-links` | `/sandbox/api/v1/payment-links` | |
| 98 | +| List | `GET` | `/api/v1/payment-links` | `/sandbox/api/v1/payment-links` | |
| 99 | +| Get | `GET` | `/api/v1/payment-links/{id}` | `/sandbox/api/v1/payment-links/{id}` | |
| 100 | +| Deactivate | `POST` | `/api/v1/payment-links/{id}/deactivate` | `/sandbox/api/v1/payment-links/{id}/deactivate` | |
| 101 | + |
| 102 | +Controlled by `CDP_SANDBOX` env var. See [Sandbox docs](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/sandbox). |
| 103 | + |
| 104 | +### Our API Endpoints |
| 105 | + |
| 106 | +**User-facing** (under `/api/v1/billing/coinbase/`): |
| 107 | + |
| 108 | +| Operation | Method | Path | Auth | |
| 109 | +|---|---|---|---| |
| 110 | +| Create Payment Link | `POST` | `/api/v1/billing/coinbase/payment-links` | User (Cognito JWT) | |
| 111 | +| Get Payment Link | `GET` | `/api/v1/billing/coinbase/payment-links/{id}` | User (Cognito JWT) | |
| 112 | + |
| 113 | +Implementation: `src/api/v1/billing/coinbase.py` |
| 114 | + |
| 115 | +**Admin-only** (under `/api/v1/billing/`, requires `X-Admin-Secret`): |
| 116 | + |
| 117 | +| Operation | Method | Path | Auth | |
| 118 | +|---|---|---|---| |
| 119 | +| List Payment Links | `GET` | `/api/v1/billing/payment-links` | Admin (X-Admin-Secret) | |
| 120 | +| Deactivate Payment Link | `POST` | `/api/v1/billing/payment-links/{id}/deactivate` | Admin (X-Admin-Secret) | |
| 121 | + |
| 122 | +Implementation: `src/api/v1/billing/admin.py` |
| 123 | + |
| 124 | +### Key Differences from Commerce Charges |
| 125 | + |
| 126 | +| Aspect | Commerce (old) | Payment Link (new) | |
| 127 | +|---|---|---| |
| 128 | +| ID format | UUID | 24-char hex | |
| 129 | +| URL field | `hosted_url` | `url` | |
| 130 | +| Amount | `pricing.local.amount` | `amount` (flat) | |
| 131 | +| Currency | `pricing.local.currency` | `currency` (flat) | |
| 132 | +| Status | `timeline` array | Single `status` field | |
| 133 | +| Statuses | NEW, SIGNED, PENDING, COMPLETED | ACTIVE, COMPLETED, EXPIRED, DEACTIVATED | |
| 134 | +| Currencies | BTC, ETH, USDC, DAI, USD | **USDC only** | |
| 135 | +| Network | Multiple | **Base only** | |
| 136 | +| Idempotency | Not required | `X-Idempotency-Key` header | |
| 137 | + |
| 138 | +--- |
| 139 | + |
| 140 | +## 6. Currency Limitation — Important |
| 141 | + |
| 142 | +The Payment Link API currently **only supports USDC on Base network**. If users were previously paying with BTC, ETH, or other currencies via Commerce, they will need to use USDC going forward. |
| 143 | + |
| 144 | +Verify: |
| 145 | +- [ ] Frontend payment UI reflects USDC-only |
| 146 | +- [ ] Any documentation/help text referencing multi-currency is updated |
| 147 | +- [ ] Pricing is displayed in USDC (1:1 with USD) |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +## 7. Database / Data Migration |
| 152 | + |
| 153 | +No schema changes required — the `credits_ledger` table already supports both formats via the `payment_metadata` JSONB column. |
| 154 | + |
| 155 | +### Verify |
| 156 | +- [ ] Existing `payment_metadata` entries with `"type": "charge"` remain queryable |
| 157 | +- [ ] New entries will have `"type": "payment_link"` |
| 158 | +- [ ] `external_transaction_id` now stores 24-char hex IDs (was UUID charge codes) |
| 159 | +- [ ] No migration script needed for existing data |
| 160 | + |
| 161 | +--- |
| 162 | + |
| 163 | +## 8. User Identification in Payment Flow |
| 164 | + |
| 165 | +The `metadata` field on the payment link is used to pass the user identifier through the Coinbase payment flow. Coinbase treats metadata as an opaque key-value store and returns it verbatim in webhook payloads. |
| 166 | + |
| 167 | +### Flow |
| 168 | + |
| 169 | +1. **Create** (`POST /api/v1/billing/coinbase/payment-links`): |
| 170 | + The authenticated user's `cognito_user_id` is **automatically injected** into `metadata.user_id` server-side. The caller cannot override this — it is set from the JWT-authenticated session. |
| 171 | + |
| 172 | + ```json |
| 173 | + // Sent to Coinbase API: |
| 174 | + { |
| 175 | + "amount": "10.00", |
| 176 | + "currency": "USDC", |
| 177 | + "metadata": { |
| 178 | + "user_id": "<cognito_user_id>" |
| 179 | + } |
| 180 | + } |
| 181 | + ``` |
| 182 | + |
| 183 | +2. **Webhook** (`POST /api/v1/webhooks/coinbase`): |
| 184 | + Coinbase sends the `metadata` back in the event payload. The webhook handler reads `metadata.user_id` to look up the user and credit their account. |
| 185 | + |
| 186 | + ```json |
| 187 | + // Received from Coinbase: |
| 188 | + { |
| 189 | + "id": "69163c762331ed43dc64a6ef", |
| 190 | + "eventType": "payment_link.payment.success", |
| 191 | + "amount": "10.00", |
| 192 | + "currency": "USDC", |
| 193 | + "metadata": { |
| 194 | + "user_id": "<cognito_user_id>" |
| 195 | + }, |
| 196 | + ... |
| 197 | + } |
| 198 | + ``` |
| 199 | + |
| 200 | +3. **Credit**: The webhook service looks up the user by `cognito_user_id`, validates the amount, and creates a purchase ledger entry. |
| 201 | + |
| 202 | +### Implementation |
| 203 | +- Injection: `src/api/v1/billing/coinbase.py` → `metadata["user_id"] = current_user.cognito_user_id` |
| 204 | +- Extraction: `src/services/coinbase_webhook_service.py` → `_get_user_from_metadata()` |
| 205 | + |
| 206 | +--- |
| 207 | + |
| 208 | +## 9. Transition Architecture (Dual Webhook Support) |
| 209 | + |
| 210 | +During the transition period, the system supports **both** Commerce and Payment Link webhooks simultaneously: |
| 211 | + |
| 212 | +``` |
| 213 | +POST /api/v1/webhooks/coinbase |
| 214 | + ├── X-Hook0-Signature header present → Payment Link handler (new) |
| 215 | + └── X-CC-Webhook-Signature header present → Legacy Commerce handler (deprecated) |
| 216 | +``` |
| 217 | + |
| 218 | +### Files involved: |
| 219 | +- `src/api/v1/webhooks/coinbase.py` — Dual-format webhook endpoint with auto-detection |
| 220 | +- `src/services/coinbase_webhook_service.py` — Event handlers for both formats |
| 221 | +- `src/api/v1/billing/coinbase.py` — New Payment Link CRUD endpoints (user-facing) |
| 222 | +- `src/services/coinbase_payment_link_service.py` — Payment Link API client |
| 223 | +- `src/services/coinbase_auth.py` — CDP JWT auth for API calls |
| 224 | +- `src/core/config.py` — Both `COINBASE_COMMERCE_WEBHOOK_SECRET` and `COINBASE_PAYMENT_LINK_WEBHOOK_SECRET` |
| 225 | + |
| 226 | +--- |
| 227 | + |
| 228 | +## 10. Testing Checklist |
| 229 | + |
| 230 | +### Sandbox Testing |
| 231 | +- [ ] Set `CDP_SANDBOX=true` and verify API calls go to `/sandbox/api/v1/payment-links` |
| 232 | +- [ ] Create a sandbox payment link and complete payment with [testnet USDC](https://portal.cdp.coinbase.com/products/faucet) |
| 233 | +- [ ] Register a sandbox webhook subscription (with `"sandbox": "true"` label) |
| 234 | +- [ ] Verify sandbox webhook events are received and processed correctly |
| 235 | + |
| 236 | +### Pre-deployment |
| 237 | +- [ ] Verify JWT signing works with test CDP key |
| 238 | +- [ ] Create a test payment link via `POST /api/v1/billing/coinbase/payment-links` |
| 239 | +- [ ] Verify webhook signature verification with test payload (both formats) |
| 240 | +- [ ] Test idempotency (same webhook delivered twice) |
| 241 | +- [ ] Test expired/failed webhook handling |
| 242 | +- [ ] Verify legacy Commerce webhooks still work during transition |
| 243 | + |
| 244 | +### Post-deployment |
| 245 | +- [ ] Create a real payment link and complete payment |
| 246 | +- [ ] Verify credits appear in user's balance |
| 247 | +- [ ] Verify payment metadata is stored correctly |
| 248 | +- [ ] Monitor logs for `coinbase_pl_webhook_*` events |
| 249 | +- [ ] Verify deactivation works |
| 250 | + |
| 251 | +### Postman Collection |
| 252 | +Coinbase provides a [Postman collection](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/postman-files) for testing all Payment Link API endpoints. |
| 253 | + |
| 254 | +--- |
| 255 | + |
| 256 | +## 11. Rollback Plan |
| 257 | + |
| 258 | +If issues are discovered after deployment: |
| 259 | + |
| 260 | +1. The webhook endpoint supports both `X-Hook0-Signature` and `X-CC-Webhook-Signature` — legacy Commerce continues to work without code changes |
| 261 | +2. CDP API key credentials coexist with Commerce API keys during transition |
| 262 | +3. Both `COINBASE_COMMERCE_WEBHOOK_SECRET` and `COINBASE_PAYMENT_LINK_WEBHOOK_SECRET` can be set simultaneously |
| 263 | + |
| 264 | +--- |
| 265 | + |
| 266 | +## 12. Post-Migration Cleanup (after transition) |
| 267 | + |
| 268 | +Once all Commerce charges have settled and new system is confirmed working: |
| 269 | + |
| 270 | +- [ ] Remove `COINBASE_COMMERCE_WEBHOOK_SECRET` from config |
| 271 | +- [ ] Remove `_detect_webhook_format()` and `verify_legacy_commerce_signature()` from webhook handler |
| 272 | +- [ ] Remove `_handle_legacy_commerce_webhook()` from webhook handler |
| 273 | +- [ ] Remove legacy event types and `handle_charge_confirmed()` from webhook service |
| 274 | +- [ ] Update webhook endpoint to only accept `X-Hook0-Signature` |
| 275 | + |
| 276 | +--- |
| 277 | + |
| 278 | +## 13. IP Allowlisting |
| 279 | + |
| 280 | +- [ ] If using CDP API key IP allowlisting, ensure all API server IPs are added |
| 281 | +- [ ] If behind a load balancer, verify the outbound IP (NAT gateway) is allowlisted |
| 282 | + |
| 283 | +--- |
| 284 | + |
| 285 | +## 14. Monitoring & Alerting |
| 286 | + |
| 287 | +Ensure alerts are configured for these log event types: |
| 288 | + |
| 289 | +**Payment Link (new):** |
| 290 | +- `coinbase_pl_webhook_not_configured` — Secret missing (critical) |
| 291 | +- `coinbase_pl_webhook_invalid_signature` — Signature mismatch (security) |
| 292 | +- `coinbase_pl_webhook_replay` — Replay attack attempt (security) |
| 293 | +- `coinbase_payment_link_error` — API call failures (operational) |
| 294 | + |
| 295 | +**Legacy Commerce (transition period):** |
| 296 | +- `coinbase_legacy_webhook_not_configured` — Legacy secret missing |
| 297 | +- `coinbase_legacy_webhook_invalid_signature` — Legacy signature mismatch |
| 298 | + |
| 299 | +--- |
| 300 | + |
| 301 | +## 15. Documentation References |
| 302 | + |
| 303 | +- [Migration Overview](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/migrate/overview) |
| 304 | +- [API & Schema Mapping](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/migrate/api-schema-mapping) |
| 305 | +- [Payment Link API Reference](https://docs.cdp.coinbase.com/api-reference/business-api/rest-api/payment-links/introduction) |
| 306 | +- [CDP API Key Auth](https://docs.cdp.coinbase.com/api-reference/v2/authentication) |
| 307 | +- [Webhook Docs](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/webhooks) |
| 308 | +- [Sandbox Environment](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/sandbox) |
| 309 | +- [Testnet Faucet (Base Sepolia USDC)](https://portal.cdp.coinbase.com/products/faucet) |
| 310 | +- [Migration FAQ](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/migrate/faq) |
0 commit comments