- Only allowlisted Base mainnet USDC config is currently supported by paywall MVP.
- assetData accepts JSON payload that will be returned on unlock.
curl -X POST '/api/paywall/products' \
-H 'Content-Type: application/json' \
-d '{"title":"Pro Research Report","slug":"pro-research-report","description":"Unlocks a JSON payload","amount":"5","assetData":{"report":"alpha"}}'Example success response
{
"product": {
"productId": "prod_...",
"slug": "pro-research-report",
"amount": "5"
}
}Example failure responses
HTTP 409
{
"error": "Product slug \"pro-research-report\" already exists.",
"code": "CONFLICT"
}Recovery: Use a unique slug or omit slug and let server derive one.
HTTP 400
{
"error": "title is required.",
"code": "INVALID_PARAMS"
}Recovery: Provide required title and amount fields.
- buyerId is auto-derived from paywall cookie when omitted.
- checkoutUrl points to /paywall/[slug] detail page.
curl -X POST '/api/paywall/intents' \
-H 'Content-Type: application/json' \
-d '{"productSlug":"pro-research-report","expiryMinutes":15}'Example success response
{
"intent": {
"intentId": "intent_...",
"status": "pending_payment"
},
"product": {
"slug": "pro-research-report"
},
"paymentRequestUrl": "https://example.local/api/pay?...",
"statusUrl": "https://example.local/api/paywall/intents/intent_.../status",
"unlockUrl": "https://example.local/api/paywall/intents/intent_.../unlock"
}Example failure responses
HTTP 404
{
"error": "Product not found.",
"code": "NOT_FOUND"
}Recovery: Create/select an existing product first.
HTTP 409
{
"error": "Product is not active.",
"code": "PRODUCT_INACTIVE"
}Recovery: Use an active product.
- Status endpoint may transition pending intents to settled/expired/failed.
curl -X GET '/api/paywall/intents/:intentId/status'
Example success response
{
"intent": {
"intentId": ":intentId",
"status": "pending_payment",
"paidAmount": "0"
},
"canUnlock": false
}Example failure responses
HTTP 404
{
"error": "Intent not found.",
"code": "NOT_FOUND"
}Recovery: Provide a valid intent ID.
HTTP 502
{
"error": "Unable to verify incoming payment events right now.",
"code": "VERIFIER_UNAVAILABLE"
}Recovery: Retry polling after verifier recovers.
- Unlock token is single-use and expires.
curl -X POST '/api/paywall/intents/:intentId/unlock' \
-H 'Content-Type: application/json' \
-d '{"buyerId":"buyer_..."}'Example success response
{
"intent": {
"intentId": ":intentId",
"status": "settled"
},
"unlockToken": "eyJ...",
"expiresAt": "2026-01-01T00:00:00.000Z",
"assetId": ":assetId",
"dataUrl": "https://example.local/api/paywall/data/:assetId"
}Example failure responses
HTTP 409
{
"error": "Intent is pending_payment. Payment must be settled before unlock.",
"code": "INTENT_NOT_SETTLED"
}Recovery: Wait for settled status before requesting unlock.
HTTP 403
{
"error": "Intent does not belong to this buyer.",
"code": "FORBIDDEN"
}Recovery: Use the same buyer session that created the intent.
- Successful fetch consumes token and marks intent delivered.
curl -X GET '/api/paywall/data/:assetId' \
-H 'Authorization: Bearer <unlockToken>'
Example success response
{
"contentType": "application/json",
"payload": {
"report": "alpha"
}
}Example failure responses
HTTP 403
{
"error": "Unlock token already used.",
"code": "FORBIDDEN"
}Recovery: Request a new unlock grant for another fetch.
HTTP 401
{
"error": "Missing unlock token. Use Authorization: Bearer <token>.",
"code": "UNAUTHORIZED"
}Recovery: Attach bearer token in Authorization header.