Skip to main content

Commerce

Commerce-related fields surfaced by storefront endpoints, plus the in-app purchase redirect that turns a purchase_link into a completed transaction.


Storefront Mode (scope=all)

The library, library filter, and store search endpoints accept an optional scope query parameter:

ValueBehavior
ownedDefault. Returns only content the user already has access to. Commerce fields are not included.
allStorefront mode. Returns all eligible content (owned and unowned), with commerce fields per issue.

scope=all is what Fenice uses to render the in-app store. The same response shape that powers the user's library is reused for browsing purchasable content.

Endpoints that support scope=all:

info

scope=all is a single-tenant concept and only works on endpoints that already require X-Farfalla-Tenant-Id. Multi-tenant endpoints (e.g. /user/my-content, /search/global) ignore the parameter and never include commerce fields, because pricing and purchase URLs depend on tenant context.


Commerce Fields

When scope=all is set, every issue object in the response is augmented with the following fields:

{
"id": 153598,
"tenant_id": 42,
"name": "The Governor and the Rebel",
"slug": "the-governor-and-the-rebel",
"cover_url": "https://cdn.publica.la/issues/cover.jpg",
"type": "epub",
"...": "...",

"can_be_read": false,
"can_be_bought": true,
"access_reason": null,
"free": false,
"prices": [
{
"currency": "USD",
"amount": 9.99
}
],
"purchase_link": "https://{store_final_domain}/app/purchase/153598",
"status": "Acquire"
}
FieldTypeDescription
can_be_readbooleanWhether the user already has access to read this issue
can_be_boughtbooleanWhether this issue is available for purchase (false if already owned or free)
access_reasonstring or nullWhy the user has access. One of free, purchased, subscription. Null when the user does not have access. See Access Reason below
freebooleanWhether the issue is freely available without any transaction
pricesarrayPrice list. Currently contains only the tenant's main currency. See Prices Array below
purchase_linkstring or nullBase URL to start the in-app purchase flow. The app appends ?token={jwt}&app_tenant_id={id} at click time. See Purchase Redirect below
statusstringLocalized user-facing label: "Acquire", "Read", "Listen", etc. Use this to render the action button

Access Reason

When can_be_read is true, access_reason indicates why the user has access:

ValueMeaning
freeThe issue is free (free_issue) or the tenant has free access enabled (free_access)
purchasedThe user acquired the issue: bought, gifted, assigned by admin, retail license, or external permissions
subscriptionThe user has access via a subscription plan (global, with collections, external, or entry-point based)
nullThe user does not have access (can_be_read is false)

Prices Array

The prices field is an array of price objects. Currently returns only the tenant's main currency; the array format allows future multi-currency support without breaking the contract.

Each price object:

FieldTypeDescription
currencystringISO 4217 currency code (e.g. "USD")
amountfloatBase price in major units (e.g. 9.99)
discountobjectPresent only when a promotional price is active
discount.amountfloatDiscounted price the user pays (e.g. 4.99)
discount.percentagefloat / nullDiscount percentage (e.g. 50.0)
discount.ends_atstring / nullWhen the promotion ends (ISO 8601 datetime), null if no expiry

Without discount:

"prices": [
{ "currency": "USD", "amount": 9.99 }
]

With active promotion:

"prices": [
{
"currency": "USD",
"amount": 9.99,
"discount": {
"amount": 4.99,
"percentage": 50.0,
"ends_at": "2025-08-01 00:00:00"
}
}
]

Free issue or no prices configured:

"prices": []
tip

When rendering prices, iterate the array. If an item has a discount key, show discount.amount as the current price and amount as the strikethrough original. The array will contain at most one element per currency.

Content Visibility

scope=all only returns content that is individually acquirable: issues that are free or have individual prices set. Subscription-only content (no individual prices, not free) is excluded from the results. This ensures every item returned can be meaningfully acted upon by the consumer.

Status Label Logic

The status field encodes the next action the user can take on the issue:

ConditionStatus (English)
User has access and content is audioListen
User has access and content is not audioRead
User does not have accessAcquire

Labels are translated based on the user's locale.


Purchase Redirect

GET /app/purchase/{issue_id}

Hybrid endpoint that bridges the JWT-authenticated mobile app with the cookie-based web checkout. The Fenice app opens this URL in a WebView (or external browser) and the server orchestrates the entire purchase flow:

  1. Validates the JWT bearer/query token, extracts the user email
  2. Finds or creates the user in the tenant (resolved automatically from the request domain)
  3. Logs the user in (web session + cookie)
  4. Stores the return deeplink in the session
  5. Clears the cart and adds the issue to it
  6. Returns a 302 redirect to the web checkout

When the user completes the checkout, the server detects the stored return deeplink and redirects to it instead of rendering the standard web thank you page, so the app reopens automatically and can refresh the user's library.

info

This endpoint is not under the /api/v2/ prefix because it needs to operate on the web middleware stack (cookies, sessions, full Livewire flow). It is served from the same domain as the rest of the API.

Client Integration

The purchase_link from storefront responses is a base URL. The app must append parameters before opening it:

  1. token (required): the user's current JWT.
  2. app_tenant_id (only for custom branded apps): the tenant ID that owns the app (the same value sent in the X-CustomFenice-Tenant-Id header), not necessarily the tenant that owns the content being purchased. Only send this when the calling app is a custom branded app (e.g. Bajalibros). Without it, the post-purchase deeplink defaults to publicala:// instead of the custom app's scheme, and the user won't return to the correct app. For the generic publica.la Reader, omit this parameter.
{purchase_link}?token={current_jwt}&app_tenant_id={app_tenant_id}

Parameters

ParameterInTypeRequiredDescription
issue_idpathintegerYesID of the issue to purchase
tokenquerystringYesJWT token for user authentication
app_tenant_idqueryintegerYes for custom branded appsTenant ID that owns the calling app. Used to resolve the post-purchase deeplink scheme. Custom branded apps must always send this.

Request Example

# The app appends token and app_tenant_id to the purchase_link at click time
curl -L "https://{store_final_domain}/app/purchase/153598?token=eyJhbG...&app_tenant_id=42"

Response

302 Found

Location: https://{store_final_domain}/cart/checkout?automatically_open_checkout=true

The body is empty. The client should follow the redirect inside the WebView to land on the checkout page.

Post-Purchase Deeplink

After a successful payment, the user is redirected to:

{resolved_scheme}purchase-complete?issue_id={issue_id}&status=success

The scheme is resolved server-side based on app_tenant_id:

  • No app_tenant_id (or app_tenant_id=1): uses publicala:// (publica.la Reader)
  • app_tenant_id matches the current tenant id or its aggregator_id: uses the scheme at tenants_meta.mobile_app.scheme on the tenant with id = app_tenant_id (the family owner), e.g. bajalibros-pla://
  • Otherwise: uses publicala://

For the default app:

publicala://purchase-complete?issue_id=153598&status=success

For a custom app (tenant 42 with scheme bajalibros-pla://):

bajalibros-pla://purchase-complete?issue_id=153598&status=success

The mobile app should register this deeplink path in its navigation linking config to handle the return and refresh the user's library.

Error Responses

CodeCause
401Missing token, invalid signature, or expired JWT
404Issue not found in this tenant
422Issue is free, uses pay-per-use licensing, or has no prices
429Rate limit exceeded (60 requests per minute per IP)

See Also

  • Library - Library endpoints with scope=all support
  • Search - Store search with scope=all support
  • User - Multi-tenant endpoints (always exclude commerce fields)
X

Graph View