Skip to content

Commit 8a3de23

Browse files
committed
feat: update api
1 parent 4a91bec commit 8a3de23

13 files changed

Lines changed: 673 additions & 223 deletions

docker-compose.local.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ services:
7272
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
7373
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
7474

75+
# Coinbase Business / CDP (new Payment Link API)
76+
CDP_API_KEY_ID: ${CDP_API_KEY_ID:-}
77+
CDP_API_KEY_SECRET: ${CDP_API_KEY_SECRET:-}
78+
CDP_SANDBOX: ${CDP_SANDBOX:-true}
79+
COINBASE_PAYMENT_LINK_WEBHOOK_SECRET: ${COINBASE_PAYMENT_LINK_WEBHOOK_SECRET:-}
80+
7581
LOCAL_TESTING_MODE: ${LOCAL_TESTING_MODE:-false}
7682

7783
# Cognito authentication (ensure these are passed from .env.local)

docs/coinbase-business-migration-backend-review.md

Lines changed: 157 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,31 @@
1010
- [ ] **Create Coinbase Business account** (or convert existing Commerce account)
1111
- [Getting Started](https://docs.cdp.coinbase.com/coinbase-business/introduction/get-started)
1212
- [ ] **Complete KYB (Know Your Business) verification** if not already done
13-
- [ ] **Generate CDP API Key** in the [CDP Portal](https://portal.cdp.coinbase.com/access/api)
14-
- Select **ES256** algorithm
15-
- Enable **View** scope (covers Payment Link CRUD)
16-
- Download the private key PEM file — it's shown only once
17-
- Note the key name format: `organizations/{org_id}/apiKeys/{key_id}`
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
1819

1920
---
2021

2122
## 2. Environment Variables to Configure
2223

2324
| Variable | Description | Where |
2425
|---|---|---|
25-
| `CDP_API_KEY_NAME` | Key name from CDP portal (`organizations/{org_id}/apiKeys/{key_id}`) | All environments |
26-
| `CDP_API_KEY_PRIVATE_KEY` | EC private key PEM (newlines as `\n`) | All environments (secrets manager) |
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 |
2729
| `COINBASE_PAYMENT_LINK_WEBHOOK_SECRET` | From webhook subscription metadata | All environments |
2830

29-
### Variables to Remove
31+
### Variables to Keep (Legacy - Transition Period)
3032

3133
| Variable | Reason |
3234
|---|---|
33-
| `COINBASE_COMMERCE_WEBHOOK_SECRET` | Legacy Commerce API — no longer used |
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.
3438
3539
---
3640

@@ -52,8 +56,10 @@
5256
- Format: `t=<timestamp>,h=<header_names>,v1=<hmac_sha256>`
5357
- Replay protection: Rejects events older than 5 minutes
5458

55-
### Old Format (removed)
56-
- Header: `X-CC-Webhook-Signature` — no longer accepted by our endpoint
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()`
5763

5864
---
5965

@@ -67,31 +73,53 @@ X-CC-Version: 2018-03-22
6773

6874
### New (CDP / Business)
6975
```
70-
Authorization: Bearer <ES256_JWT>
76+
Authorization: Bearer <signed_JWT>
7177
Content-Type: application/json
7278
```
7379

74-
The JWT is generated **per request** with:
75-
- `sub`: CDP key name
76-
- `iss`: `"cdp"`
77-
- `uri`: `"{METHOD} api.coinbase.com{PATH}"`
78-
- `exp`: current time + 120 seconds
79-
- `nonce`: random hex
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
8084

8185
Implementation: `src/services/coinbase_auth.py`
8286

8387
---
8488

8589
## 5. Payment Link API Endpoints
8690

87-
Base URL: `https://api.coinbase.com`
91+
### Coinbase Business API (upstream)
8892

89-
| Operation | Method | Path |
90-
|---|---|---|
91-
| Create | `POST` | `/api/v1/payment-links` |
92-
| List | `GET` | `/api/v1/payment-links` |
93-
| Get | `GET` | `/api/v1/payment-links/{id}` |
94-
| Deactivate | `POST` | `/api/v1/payment-links/{id}/deactivate` |
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`
95123

96124
### Key Differences from Commerce Charges
97125

@@ -132,14 +160,86 @@ No schema changes required — the `credits_ledger` table already supports both
132160

133161
---
134162

135-
## 8. Testing Checklist
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
136235

137236
### Pre-deployment
138237
- [ ] Verify JWT signing works with test CDP key
139-
- [ ] Create a test payment link via admin endpoint
140-
- [ ] Verify webhook signature verification with test payload
238+
- [ ] Create a test payment link via `POST /api/v1/billing/coinbase/payment-links`
239+
- [ ] Verify webhook signature verification with test payload (both formats)
141240
- [ ] Test idempotency (same webhook delivered twice)
142241
- [ ] Test expired/failed webhook handling
242+
- [ ] Verify legacy Commerce webhooks still work during transition
143243

144244
### Post-deployment
145245
- [ ] Create a real payment link and complete payment
@@ -153,38 +253,58 @@ Coinbase provides a [Postman collection](https://docs.cdp.coinbase.com/coinbase-
153253

154254
---
155255

156-
## 9. Rollback Plan
256+
## 11. Rollback Plan
157257

158258
If issues are discovered after deployment:
159259

160-
1. The webhook endpoint only accepts `X-Hook0-Signature` — if you need to revert, restore the legacy `verify_legacy_commerce_signature` function from git history
161-
2. CDP API key credentials can coexist with Commerce API keys during transition
162-
3. The `COINBASE_COMMERCE_WEBHOOK_SECRET` config was removed from code but the env var can remain set harmlessly
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
163263

164264
---
165265

166-
## 10. IP Allowlisting
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
167279

168280
- [ ] If using CDP API key IP allowlisting, ensure all API server IPs are added
169281
- [ ] If behind a load balancer, verify the outbound IP (NAT gateway) is allowlisted
170282

171283
---
172284

173-
## 11. Monitoring & Alerting
285+
## 14. Monitoring & Alerting
174286

175287
Ensure alerts are configured for these log event types:
288+
289+
**Payment Link (new):**
176290
- `coinbase_pl_webhook_not_configured` — Secret missing (critical)
177291
- `coinbase_pl_webhook_invalid_signature` — Signature mismatch (security)
178292
- `coinbase_pl_webhook_replay` — Replay attack attempt (security)
179-
- `admin_payment_link_error` — API call failures (operational)
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
180298

181299
---
182300

183-
## 12. Documentation References
301+
## 15. Documentation References
184302

185303
- [Migration Overview](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/migrate/overview)
186304
- [API & Schema Mapping](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/migrate/api-schema-mapping)
187305
- [Payment Link API Reference](https://docs.cdp.coinbase.com/api-reference/business-api/rest-api/payment-links/introduction)
188-
- [CDP API Key Auth](https://docs.cdp.coinbase.com/coinbase-business/authentication-authorization/api-key-authentication)
306+
- [CDP API Key Auth](https://docs.cdp.coinbase.com/api-reference/v2/authentication)
189307
- [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)
190310
- [Migration FAQ](https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/migrate/faq)

env.example

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,16 @@ STRIPE_WEBHOOK_SECRET=whsec_your_webhook_signing_secret
160160
# =============================================================================
161161
# COINBASE BUSINESS CONFIGURATION
162162
# =============================================================================
163-
# CDP API Key for Payment Link CRUD operations
164-
# Generate at: https://portal.cdp.coinbase.com/access/api
165-
# Key name format: organizations/{org_id}/apiKeys/{key_id}
166-
CDP_API_KEY_NAME=organizations/your-org-id/apiKeys/your-key-id
167-
# EC private key in PEM format (replace newlines with \n)
168-
CDP_API_KEY_PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----\nYOUR_KEY_HERE\n-----END EC PRIVATE KEY-----"
163+
# CDP Secret API Key for Payment Link CRUD operations
164+
# Generate at: https://portal.cdp.coinbase.com/projects/api-keys (Secret API Keys tab)
165+
# Docs: https://docs.cdp.coinbase.com/api-reference/v2/authentication
166+
# Key ID: UUID from the CDP portal
167+
CDP_API_KEY_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
168+
# Key Secret: base64-encoded secret from the CDP portal
169+
CDP_API_KEY_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
170+
# Set to true for sandbox (no real transactions, uses Base Sepolia testnet)
171+
# See: https://docs.cdp.coinbase.com/coinbase-business/payment-link-apis/sandbox
172+
CDP_SANDBOX=false
169173

170174
# Payment Link webhook signature verification secret
171175
# From metadata.secret when creating a webhook subscription

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ boto3 = "^1.34.0"
2626
structlog = "^23.2.0"
2727
python-multipart = "0.0.20"
2828
stripe = "^11.0.0"
29-
web3 = "^6.0.0"
30-
eth-account = "^0.10.0"
29+
cdp-sdk = "^1.4.0"
30+
web3 = "^7.0.0"
31+
eth-account = "^0.13.0"
3132
redis = "^5.0.0"
32-
siwe = "^4.0.0"
33+
siwe = "^4.4.0"
3334

3435
[tool.poetry.group.dev.dependencies]
3536
pytest = "^7.4.0"

src/api/v1/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .audio.index import router as audio_router
1010
from .billing.index import router as billing_router
1111
from .billing.admin import admin_router as billing_admin_router
12+
from .billing.coinbase import coinbase_billing_router
1213
from .webhooks.stripe import stripe_webhook_router
1314
from .webhooks.coinbase import coinbase_webhook_router
1415
from .wallet.index import router as wallet_router
@@ -37,6 +38,7 @@
3738
# Billing router
3839
billing = APIRouter()
3940
billing.include_router(billing_router)
41+
billing.include_router(coinbase_billing_router)
4042

4143
# Billing admin router (separate Swagger page at /admin/docs)
4244
billing_admin = APIRouter()

0 commit comments

Comments
 (0)