Skip to content

Read a distributor's Odoo product and subscription catalogue (user)

THE server-to-server endpoint of this surface. A distributor calls it from their own system to read their Odoo products and, if their token allows, their subscriptions. Every other Odoo endpoint is private to the cockpit partner interface.

Auth

"Public" here means "not the cockpit UI session". It is NOT unauthenticated. Send a Partner-owned Sanctum bearer token carrying the odoo-catalogue ability.

THE PARTNER IN THE PATH IS FOR READABILITY ONLY. The token decides which Odoo is read, and the request 404s unless the token's partner matches the path. A partner id is an identifier, not a secret, so a mismatch cannot be allowed to succeed. It answers 404 rather than 403 on purpose: confirming that some other partner id exists is itself a disclosure.

Unlike the session endpoints, this route is NOT behind the partner interface kill switch, so it never answers 503.

Two abilities, not one

odoo-catalogue gets you products. Subscriptions need odoo-subscriptions ON TOP, because that block carries customer names and recurring_monthly / recurring_total / non_recurring_total for every sale order including drafts and churned deals. That is a commercial ledger rather than a price list, so it has to be asked for separately.

Reading the subscription block

The three subscription keys are ALWAYS present, and subscriptions: [] alone never tells you why. Read subscriptions_available first, then subscriptions_unavailable_reason, which distinguishes three different situations a caller must act on differently: the token lacks the ability, the partner's Odoo lacks the Enterprise sale_subscription app, or the read failed. Products are still returned in all three cases, deliberately, rather than losing what the caller came for.

HTTP: bearerAuth

User bearer token. Default authentication for customer-facing endpoints. Obtain a token by calling POST /login with your credentials, then send it on every subsequent request as Authorization: Bearer <token>. The token inherits the permissions and PBX scope of the authenticated user.

HTTP Authorization Scheme
bearer
Bearer format
Bearer <token>
partner path · string · uuid *
The cockpit Partner UUID. Must match the token's own partner or the answer is 404.
limit query · integer
Caps BOTH blocks. Out-of-range values are refused rather than clamped, unlike an earlier revision of this endpoint.
accept header · string
example: application/json

Responses

Response schema
messagestring
example: Catalogue retrieved
dataobject
products*array<object>
Each item — A product as the PUBLIC catalogue exposes it. Deliberately narrower and flatter than the internal product model: `category` and `unit` are flattened to their labels, because an Odoo primary key is not useful to a third party and pins them to one instance.
idinteger
example: 812
namestring | null
example: Desk Lamp
referencestring | null
The internal model calls this `code` (Odoo's `default_code`).
example: LAMP-001
pricenumber · float
Sales price. Always a number, 0 when Odoo has none.
example: 89.5
currencystring | null
Currency NAME.
example: EUR
categorystring | null
Category NAME only, already the full path. Not an object here.
example: All / Office Furniture
unitstring | null
Unit of measure NAME only. Not an object here.
example: Units
activeboolean
example: 1
subscriptions*array<object>
EMPTY does not mean "none". Check `subscriptions_available` before concluding anything from an empty array.
Each item — A subscription as the PUBLIC catalogue exposes it. Narrower than the internal subscription model: the order's own tax totals and `customer_reference` are not disclosed. Note the shape is not uniformly flattened. `plan` is reduced to its name, but `customer` stays an object, unlike every other label on this model.
idinteger
example: 7731
namestring | null
example: S00042
customerobject | null
Still an object here, unlike `plan`.
idinteger
example: 315
namestring | null
example: Northwind Trading
planstring | null
Plan NAME only.
example: Monthly
statusstring | null
Odoo's own raw `subscription_state`, as on the internal model.
enum: 1_draft 2_renewal 3_progress 4_paused 5_renewed 6_churn 7_upsell null
example: 3_progress
recurring_monthlynumber · float
example: 49.9
recurring_totalnumber · float
example: 49.9
non_recurring_totalnumber · float
example: 0
next_invoice_datestring | null · date
example: 2026-09-01
currencystring | null
example: EUR
order_datestring | null · date-time
example: 2026-03-01 08:00:00
subscriptions_available*boolean
False when the block was withheld or could not be read, for any of the three reasons named below.
example: 1
subscriptions_unavailable_reason*string | null
Null when `subscriptions_available` is true. Otherwise one of three distinct causes, which need different fixes: the token has no `odoo-subscriptions` ability (ask for a token that does), the instance lacks the Enterprise subscription app (a licensing matter), or the read failed (retry).
No valid bearer token.
Response schema
messagestring
The token is valid but not usable here: it is not owned by a Partner, it lacks the `odoo-catalogue` ability, or the partner's own status is not Active.
Response schema
messagestring
The path names a different partner than the token belongs to, or names none at all. Deliberately 404 and not 403: a wrong id must be indistinguishable from a nonexistent one from outside.
Response schema
messagestring
TWO DIFFERENT BODIES share this status, and a client must handle both. Request validation failures carry `errors` and no `message`. Everything else (integration not configured, Odoo Sign app absent, an operation Odoo refused, a filter too broad for one Odoo domain to express, an Idempotency-Key reused for a different request) carries `message` and no `errors`. A too-broad filter is 422 rather than 502 because the request is answerable, just not as asked: narrowing it (a date range, a single `partner_id`) is something only the caller can decide, and retrying it unchanged cannot help.
Response schema
oneOf
errors*object
Free-form object
message*string
Rate limited: 30 requests per minute, keyed by ACCESS TOKEN rather than by partner, so each token issued to a distributor gets its own budget. One call returns a whole product and subscription list, which is why the cap is well below a page-at-a-time API's.
Response schema
messagestring
The partner's Odoo could not be reached, or answered with a fault this endpoint does not translate into a more specific status. Usually retryable. The underlying Odoo fault is logged, not returned. ALSO here, and NOT retryable: an Odoo company scope that could not be resolved for the configured user. Every aggregate on this surface is filtered by the company ids that read produces, so it refuses rather than answering with figures drawn from companies the caller may not be allowed to see. The stored integration needs fixing. A filter too broad to express is NOT here: that is a 422, because narrowing it is the caller's decision.
Response schema
messagestring
get https://cockpit.voxbi.com/api/v1/odoo/public/{partner}/catalogue
Base URL
Request sample
curl -X GET 'https://cockpit.voxbi.com/api/v1/odoo/public/{partner}/catalogue' \
  -H 'Authorization: Bearer YOUR_TOKEN'
const response = await fetch('https://cockpit.voxbi.com/api/v1/odoo/public/{partner}/catalogue', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${YOUR_TOKEN}`,
  },
});

const data = await response.json();
console.log(data);
import requests

response = requests.get('https://cockpit.voxbi.com/api/v1/odoo/public/{partner}/catalogue',
    headers={'Authorization': f'Bearer {YOUR_TOKEN}'}
)
response.raise_for_status()
data = response.json()
print(data)
<?php
$context = stream_context_create([
    'http' => [
        'method'  => 'GET',
        'header'  => "Content-Type: application/json\r\nAuthorization: Bearer YOUR_TOKEN",
    ],
]);

$response = file_get_contents('https://cockpit.voxbi.com/api/v1/odoo/public/{partner}/catalogue', false, $context);
$data = json_decode($response, true);
print_r($data);
{}
"message": "Catalogue retrieved",
"data": {}
"products": [],
{}
"id": 812,
"name": "Desk Lamp",
"reference": "LAMP-001",
"price": 89.5,
"currency": "EUR",
"category": "All / Office Furniture",
"unit": "Units",
"active": true
}
],
"subscriptions": [],
{}
"id": 7731,
"name": "S00042",
"customer": {},
"id": 315,
"name": "Northwind Trading"
},
"plan": "Monthly",
"status": "3_progress",
"recurring_monthly": 49.9,
"recurring_total": 49.9,
"non_recurring_total": 0,
"next_invoice_date": "2026-09-01",
"currency": "EUR",
"order_date": "2026-03-01 08:00:00"
}
],
"subscriptions_available": true,
"subscriptions_unavailable_reason": null
}
}
{}
"message": "Unauthenticated."
}
{}
"message": "The partner account is not active."
}
{}
"message": "Not Found."
}
{}
"errors": {}
"limit": []
"The limit field must not be greater than 200."
]
}
}
{}
"message": "Too Many Attempts."
}
Retry-After integer
Seconds until the bucket refills.
example: 22
{}
"message": "Could not retrieve invoices from Odoo."
}