Oplyon Partner API

Leggi in sola lettura i dati gestionali di un tenant Oplyon, con il consenso esplicito del titolare — tramite OAuth 2.0 con PKCE (RFC 7636).

Come funziona il flusso

Richiedi l'accesso
Compila il form di registrazione con nome dell'app, redirect URI e gli scope (dati) che ti servono. La richiesta viene revisionata dal nostro team — non è self-service istantaneo.
Ricevi le credenziali
Se approvata, ti contattiamo fuori banda con client_id e client_secret — un'unica coppia dedicata alla tua applicazione, non condivisa con altri sviluppatori.
Genera l'URL di autorizzazione
Reindirizza l'utente (il titolare del tenant Oplyon che vuoi collegare) all'endpoint /connect/authorize del suo tenant. PKCE è obbligatorio per tutti i client, anche confidenziali.
GET https://<tenant>.oplyon.com/connect/authorize
  ?response_type=code
  &client_id=IL_TUO_CLIENT_ID
  &redirect_uri=IL_TUO_REDIRECT_URI
  &scope=oplyon.products.read offline_access
  &state=VALORE_CASUALE_ANTI_CSRF
  &code_challenge=SHA256_BASE64URL(code_verifier)
  &code_challenge_method=S256
L'utente fa login e autorizza
Se non ha già una sessione attiva sul tenant, gli viene chiesto il login; poi vede una schermata di consenso con gli scope richiesti e può accettare o rifiutare.
Ricevi il code
Se l'utente autorizza, viene reindirizzato al tuo redirect_uri con ?code=...&state=....
Scambia il code con un token
Chiama /connect/token con il code, le tue credenziali e il code_verifier PKCE originale. Ricevi un access_token (JWT, 15 minuti) e un refresh_token (30 giorni, a rotazione).
POST https://<tenant>.oplyon.com/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=IL_CODE_RICEVUTO
&redirect_uri=IL_TUO_REDIRECT_URI
&client_id=IL_TUO_CLIENT_ID
&client_secret=IL_TUO_CLIENT_SECRET
&code_verifier=IL_CODE_VERIFIER_ORIGINALE
Chiama l'API
Invia l'access token come header Bearer a qualunque endpoint /api/partner/v1/....
GET https://<tenant>.oplyon.com/api/partner/v1/products
Authorization: Bearer IL_ACCESS_TOKEN
Rinnova quando scade
Usa il refresh_token con grant_type=refresh_token sullo stesso endpoint /connect/token per ottenere un nuovo access token senza richiedere di nuovo il consenso.

Scope disponibili

Ogni scope dà accesso in sola lettura a una categoria di dati. Richiedi solo quelli che ti servono davvero.

Anagrafiche
Prodotti
oplyon.products.read
Prodotti, varianti, categorie, info di scheda prodotto
GET /products
GET /products/{id}
GET /categories
Clienti
oplyon.customers.read
Anagrafica clienti
GET /customers
GET /customers/{id}
Fornitori
oplyon.suppliers.read
Anagrafica fornitori
GET /suppliers
GET /suppliers/{id}
Vendite
Ordini di vendita
oplyon.sales-orders.read
Ordini di vendita e relative righe
GET /sales-orders
GET /sales-orders/{id}
Fatture di vendita
oplyon.sales-invoices.read
Fatture e note di credito di vendita
GET /sales-invoices
GET /sales-invoices/{id}
GET /sales-credit-notes
GET /sales-credit-notes/{id}
DDT
oplyon.delivery-notes.read
Documenti di trasporto
GET /delivery-notes
GET /delivery-notes/{id}
Vendite online
oplyon.online-sales.read
Ordini/vendite dai canali online e marketplace
GET /online-sales
GET /online-sales/{id}
Vendite al dettaglio
oplyon.retail-sales.read
Scontrini e vendite POS
GET /retail-sales
GET /retail-sales/{id}
Resi
oplyon.returns.read
Resi/rientri merce
GET /returns
GET /returns/{id}
Acquisti
Ordini fornitore
oplyon.purchase-orders.read
Ordini a fornitore, incluso stato invio email
GET /purchase-orders
GET /purchase-orders/{id}
Fatture di acquisto
oplyon.purchase-invoices.read
Fatture e note di credito di acquisto
GET /purchase-invoices
GET /purchase-invoices/{id}
GET /purchase-credit-notes
GET /purchase-credit-notes/{id}
Magazzino
Magazzino
oplyon.inventory.read
Movimenti e giacenze, incluso multi-sede
GET /inventory-movements
Finanza
Incassi e pagamenti
oplyon.payments.read
Flussi gestionali di incasso/pagamento (mai conti bancari)
GET /payments
GET /payment-allocations
GET /payment-terms
Analisi finanziaria
oplyon.finance-analysis.read
Margini, costi, profitto netto/lordo aggregati
GET /finance-analysis
Statistiche di vendita
oplyon.sales-stats.read
Statistiche di vendita incluso margine/profitto per prodotto
GET /sales-stats
Logistica
Spedizioni
oplyon.shipping.read
Corrieri, tariffe, statistiche di servizio
GET /carriers
GET /carrier-services
Riferimenti
Riferimenti fiscali
oplyon.tax-reference.read
Codici IVA, nazioni, dati di riferimento a basso rischio
GET /vat-codes
GET /countries
GET /country-provinces
Tecnico
Accesso persistente
offline_access
Necessario per ricevere un refresh_token. Aggiungilo sempre se non vuoi far rifare il login all'utente ogni 15 minuti.

Esempi di codice

Un client funzionante completo per uno scope — genera la coppia PKCE, costruisce l'URL di autorizzazione, scambia il code, chiama un endpoint e rinnova il token. Scegli il tuo stack.

1. Genera una coppia PKCE (una volta per tentativo di login):

# macOS/Linux
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-64)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr -d '=' | tr '+/' '-_')

2. Apri questo URL in un browser — il titolare del tenant fa login e acconsente:

GET https://<tenant>.oplyon.com/connect/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=YOUR_REDIRECT_URI
  &scope=oplyon.products.read offline_access
  &state=RANDOM_ANTI_CSRF_VALUE
  &code_challenge=$CODE_CHALLENGE
  &code_challenge_method=S256

3. Il tuo redirect_uri riceve ?code=... — scambialo con i token:

curl -s -X POST https://<tenant>.oplyon.com/connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=THE_CODE_YOU_RECEIVED \
  -d redirect_uri=YOUR_REDIRECT_URI \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET \
  -d code_verifier=$CODE_VERIFIER

Risposta:

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "CfDJ8...",
  "scope": "oplyon.products.read offline_access"
}

4. Chiama un endpoint:

curl -s https://<tenant>.oplyon.com/api/partner/v1/products?limit=50&offset=0 \
  -H "Authorization: Bearer ACCESS_TOKEN"

5. Quando l'access token scade (15 min), rinnovalo — senza richiedere di nuovo il consenso:

curl -s -X POST https://<tenant>.oplyon.com/connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=refresh_token \
  -d refresh_token=REFRESH_TOKEN \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET

Node.js 18+ (fetch e crypto integrati, nessuna dipendenza):

import crypto from "node:crypto";

const TENANT = "<tenant>.oplyon.com";
const CLIENT_ID = "YOUR_CLIENT_ID";
const CLIENT_SECRET = "YOUR_CLIENT_SECRET";
const REDIRECT_URI = "YOUR_REDIRECT_URI";

function base64url(buf) {
  return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

// 1. PKCE pair — store codeVerifier server-side, keyed by state, until the callback arrives
const codeVerifier = base64url(crypto.randomBytes(32));
const codeChallenge = base64url(crypto.createHash("sha256").update(codeVerifier).digest());
const state = base64url(crypto.randomBytes(16));

// 2. Redirect the user's browser to this URL
const authorizeUrl = new URL(`https://${TENANT}/connect/authorize`);
authorizeUrl.search = new URLSearchParams({
  response_type: "code",
  client_id: CLIENT_ID,
  redirect_uri: REDIRECT_URI,
  scope: "oplyon.products.read offline_access",
  state,
  code_challenge: codeChallenge,
  code_challenge_method: "S256",
}).toString();

// 3. In your redirect_uri handler, exchange the received ?code=... for tokens
async function exchangeCodeForTokens(code) {
  const res = await fetch(`https://${TENANT}/connect/token`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      code_verifier: codeVerifier,
    }),
  });
  if (!res.ok) throw new Error(`token exchange failed: ${res.status}`);
  return await res.json(); // { access_token, refresh_token, expires_in, ... }
}

// 4. Call an endpoint with the access token
async function listProducts(accessToken, { limit = 50, offset = 0 } = {}) {
  const url = `https://${TENANT}/api/partner/v1/products?limit=${limit}&offset=${offset}`;
  const res = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } });
  if (res.status === 401) throw new Error("access token expired or invalid — refresh it");
  if (!res.ok) throw new Error(`API call failed: ${res.status}`);
  return await res.json(); // { items: [...], total, limit, offset }
}

// 5. Refresh when the access token expires (no new consent needed)
async function refreshAccessToken(refreshToken) {
  const res = await fetch(`https://${TENANT}/connect/token`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: refreshToken,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
    }),
  });
  if (!res.ok) throw new Error(`refresh failed: ${res.status}`);
  return await res.json();
}

.NET 8 / HttpClient:

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

const string Tenant = "<tenant>.oplyon.com";
const string ClientId = "YOUR_CLIENT_ID";
const string ClientSecret = "YOUR_CLIENT_SECRET";
const string RedirectUri = "YOUR_REDIRECT_URI";

static string Base64Url(byte[] bytes) =>
    Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_').TrimEnd('=');

// 1. PKCE pair — keep codeVerifier server-side, keyed by state, until the callback arrives
var codeVerifier = Base64Url(RandomNumberGenerator.GetBytes(32));
var codeChallenge = Base64Url(SHA256.HashData(Encoding.ASCII.GetBytes(codeVerifier)));
var state = Base64Url(RandomNumberGenerator.GetBytes(16));

// 2. Redirect the user's browser here
var authorizeUrl = $"https://{Tenant}/connect/authorize" +
    $"?response_type=code&client_id={ClientId}&redirect_uri={Uri.EscapeDataString(RedirectUri)}" +
    $"&scope=oplyon.products.read+offline_access&state={state}" +
    $"&code_challenge={codeChallenge}&code_challenge_method=S256";

using var http = new HttpClient();

// 3. In your redirect_uri handler, exchange ?code=... for tokens
async Task<JsonDocument> ExchangeCodeAsync(string code)
{
    var form = new Dictionary<string, string>
    {
        ["grant_type"] = "authorization_code",
        ["code"] = code,
        ["redirect_uri"] = RedirectUri,
        ["client_id"] = ClientId,
        ["client_secret"] = ClientSecret,
        ["code_verifier"] = codeVerifier,
    };
    var res = await http.PostAsync($"https://{Tenant}/connect/token", new FormUrlEncodedContent(form));
    res.EnsureSuccessStatusCode();
    return JsonDocument.Parse(await res.Content.ReadAsStringAsync());
}

// 4. Call an endpoint with the access token
async Task<JsonDocument> ListProductsAsync(string accessToken, int limit = 50, int offset = 0)
{
    using var req = new HttpRequestMessage(HttpMethod.Get,
        $"https://{Tenant}/api/partner/v1/products?limit={limit}&offset={offset}");
    req.Headers.Authorization = new("Bearer", accessToken);
    var res = await http.SendAsync(req);
    res.EnsureSuccessStatusCode(); // 401 here means the access token expired — refresh it
    return JsonDocument.Parse(await res.Content.ReadAsStringAsync()); // { items, total, limit, offset }
}

// 5. Refresh when the access token expires (no new consent needed)
async Task<JsonDocument> RefreshAsync(string refreshToken)
{
    var form = new Dictionary<string, string>
    {
        ["grant_type"] = "refresh_token",
        ["refresh_token"] = refreshToken,
        ["client_id"] = ClientId,
        ["client_secret"] = ClientSecret,
    };
    var res = await http.PostAsync($"https://{Tenant}/connect/token", new FormUrlEncodedContent(form));
    res.EnsureSuccessStatusCode();
    return JsonDocument.Parse(await res.Content.ReadAsStringAsync());
}

Ogni endpoint di lista accetta i parametri limit (massimo 100, default 50) e offset e restituisce { items, total, limit, offset }. Un 401 significa che l'access token manca, è scaduto o non è valido — rinnovalo. Un 403 significa che il token è valido ma non ha lo scope richiesto, oppure il tenant del token non corrisponde all'host. Gli endpoint che espongono un singolo record accettano un id in rotta, es. GET /products/{id}, e restituiscono 404 se non trovato.

Note importanti

Tutti gli endpoint richiedono HTTPS.
redirect_uri deve corrispondere esattamente (byte per byte) a uno di quelli registrati per la tua app.
L'accesso è sempre revocabile in qualsiasi momento dal titolare del tenant.
Un token valido non può mai leggere più di quanto l'interfaccia normale mostrerebbe a un utente di quel tenant.
Limite: 60 richieste al minuto per token.

Pronto a costruire la tua integrazione?

Raccontaci cosa stai costruendo — revisioneremo la richiesta e ti risponderemo.

Richiedi l'accesso ora