Langsung ke konten
↑↓ navigasi↵ bukaEsc tutup

Panduan

Membuat aplikasi (OAuth 2.0)

Aplikasi pihak ketiga bertindak atas nama merchant TheComNet lain. Merchant menyetujui aplikasi Anda sekali; Anda mendapat token yang dibatasi pada tenant mereka melalui OAuth 2.0 authorization code flow dengan PKCE.

Mendaftarkan aplikasi

Owner atau admin tenant mana pun bisa mendaftarkan aplikasi di Developer Console → Aplikasi. Sebuah aplikasi memiliki:

  • Client id (tcn_app_…) dan client secret (tcn_cs_…). Secret hanya ditampilkan satu kali; rotasi untuk mendapat secret baru, setelah itu secret lama berhenti berfungsi.
  • Redirect URI, maksimal 10, dicocokkan persis (tanpa pencocokan prefix atau wildcard). Harus https; http hanya diizinkan untuk host loopback localhost dan 127.0.0.1. Fragment (#…) tidak diizinkan.
  • Scope maksimum yang boleh diminta aplikasi. Scope-nya sama dengan API key; lihat tabel scope.
  • Opsional, URL webhook dan event-nya, dikirim untuk setiap instalasi (lihat Webhook aplikasi).

Siklus hidup aplikasi

StatusSiapa yang bisa mengotorisasiCatatan
developmentHanya tenant milik developerUntuk pengujian. Aplikasi baru dimulai di sini.
in_reviewHanya tenant milik developerDiatur oleh Ajukan review, yang mewajibkan deskripsi dan website.
publishedTenant mana punTheComNet menyetujui aplikasi.
rejectedHanya tenant milik developerCatatan review menjelaskan alasannya. Perbaiki aplikasi lalu ajukan lagi.
suspendedTidak adaSemua token berhenti berfungsi dan endpoint token menjawab unauthorized_client.

Tenant lain yang membuka authorization URL aplikasi yang belum dipublikasikan akan melihat error di TheComNet (OAUTH_APP_NOT_AVAILABLE).

Alur

  1. 1Aplikasi AndaMembuat state dan PKCE code_verifier, menyimpan keduanya di sesi pengguna, lalu me-redirect browser ke /oauth/authorize.
  2. 2TheComNetMeminta pengguna masuk, memilih akun toko (tenant) tempat mereka menjadi owner/admin, lalu menampilkan layar persetujuan.
  3. 3MerchantMenyetujui (atau menolak) scope yang diminta.
  4. 4TheComNetMe-redirect browser ke <redirect_uri>?state=…&code=… (atau error=access_denied).
  5. 5Aplikasi AndaMemeriksa state, lalu mengirim POST code + code_verifier ke /v1/oauth/token dengan client credential-nya (di sisi server).
  6. 6TheComNetMengembalikan access token (1 jam), refresh token (90 hari), scope yang diberikan, tenant_id, dan installation_id.
  7. 7Aplikasi AndaMemanggil API dengan Authorization: Bearer tcn_at_…, dan melakukan refresh sebelum access token kedaluwarsa.

1. Authorization request

Arahkan browser merchant ke https://app.comnethub.app/oauth/authorize:

GET
https://app.comnethub.app/oauth/authorize
  ?client_id=tcn_app_…
  &redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback
  &scope=orders%3Aread%20products%3Aread
  &state=Zk3q…
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
ParameterWajibArti
client_idyaClient id aplikasi Anda.
redirect_uriyaSalah satu redirect URI terdaftar, persis.
scopetidakScope dipisah spasi, subset dari scope aplikasi. Kosong atau tidak ada berarti semua scope aplikasi.
statedisarankanNilai opaque yang dikembalikan saat redirect. Ikat ke sesi pengguna dan periksa (CSRF).
code_challengedisarankanSHA-256 base64url dari code_verifier, tanpa padding (43 karakter).
code_challenge_methodbila ada challengeS256

PKCE disarankan untuk setiap aplikasi dan praktis wajib untuk public client atau native client, yang tidak bisa menyimpan secret. Bila challenge dikirim, token request harus membawa code_verifier yang cocok.

Merchant masuk, memilih akun toko (tenant) tempat mereka menjadi owner atau admin, lalu menyetujui. Tenant yang memasang butuh paket Pro (akses API); bila tidak, layar persetujuan akan menyebutkannya. Mengotorisasi ulang untuk instalasi yang sudah ada akan mengganti scope-nya.

Redirect kembali ke aplikasi Anda

Disetujui / ditolak
https://example.com/oauth/callback?state=Zk3q…&code=8fKq…
https://example.com/oauth/callback?state=Zk3q…&error=access_denied
  • Code berlaku 10 menit dan hanya bisa dipakai sekali.
  • client_id yang tidak dikenal atau redirect_uri yang tidak terdaftar ditampilkan sebagai error di TheComNet dan tidak pernah di-redirect, sehingga request palsu tidak bisa memantulkan pengguna ke situs sembarang.

2. Menukar code

Endpoint
POST https://api.comnethub.app/v1/oauth/token
  • Body: application/x-www-form-urlencoded (JSON juga diterima).
  • Autentikasi client: HTTP Basic dengan client_id:client_secret, atau client_id dan client_secret di body.
  • grant_type=authorization_code dengan code, redirect_uri (nilai yang sama seperti di authorization request), dan code_verifier (bila challenge dikirim).
curl -X POST "https://api.comnethub.app/v1/oauth/token" \
  -u "$THECOMNET_CLIENT_ID:$THECOMNET_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  --data-urlencode redirect_uri="https://example.com/oauth/callback" \
  -d code_verifier="$CODE_VERIFIER"
200 application/json · cache-control: no-store
{
  "access_token": "tcn_at_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "tcn_rt_…",
  "scope": "orders:read products:read",
  "tenant_id": "0192f0c0-1111-7aaa-8bbb-0123456789ab",
  "installation_id": "0192f0e1-2c3d-7e4f-8a5b-6c7d8e9fa0b1"
}

scope adalah yang diberikan merchant. Simpan koneksi Anda dengan kunci tenant_id (atau installation_id).

3. Refresh token

Access token berlaku satu jam (expires_in: 3600). Dapatkan pasangan baru dengan grant_type=refresh_token:

curl
curl -X POST "https://api.comnethub.app/v1/oauth/token" \
  -u "$THECOMNET_CLIENT_ID:$THECOMNET_CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN"
  • Refresh token berlaku 90 hari dan berotasi setiap dipakai: response membawa refresh token baru dan yang lama berhenti berfungsi.
  • Simpan refresh token baru secara atomik bersama access token, sebelum memakainya. Serialkan refresh per instalasi.
  • Menyerahkan refresh token yang sudah dipakai, atau memutar ulang authorization code, diperlakukan sebagai pencurian: semua token instalasi dicabut dan merchant harus mengotorisasi ulang.

Error endpoint token

Endpoint token dan revoke menjawab dalam format OAuth (RFC 6749), bukan problem details:

400 application/json
{
  "error": "invalid_grant",
  "error_description": "Authorization code expired"
}
errorStatusKapan
invalid_request400Parameter wajib tidak ada (misalnya token pada revoke).
invalid_client401Client id tidak dikenal atau secret salah. Dikirim dengan header www-authenticate: Basic.
invalid_grant400Code atau refresh token tidak dikenal, kedaluwarsa, atau dipakai ulang; redirect_uri atau code_verifier tidak cocok; aplikasi sudah dicopot.
unauthorized_client400Aplikasi sedang ditangguhkan.
unsupported_grant_type400grant_type bukan authorization_code atau refresh_token.

Tangani invalid_grant saat refresh dengan menandai koneksi sebagai terputus dan meminta merchant menghubungkan ulang. Endpoint token dan revoke dibatasi 120 request per menit per IP. Lihat juga Error.

Mencabut token

POST https://api.comnethub.app/v1/oauth/revoke dengan token=… (RFC 7009) dan autentikasi client yang sama. Selalu menjawab 200 untuk client yang terautentikasi, termasuk untuk token yang tidak dikenal.

curl
curl -X POST "https://api.comnethub.app/v1/oauth/revoke" \
  -u "$THECOMNET_CLIENT_ID:$THECOMNET_CLIENT_SECRET" \
  -d token="$REFRESH_TOKEN"
  • Mencabut refresh token mengakhiri semua token instalasi (aplikasi tetap terpasang tetapi harus diotorisasi ulang).
  • Mencabut access token hanya mengakhiri token itu.

4. Memanggil API

curl
curl "https://api.comnethub.app/v1/developer/token-info" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
  • App token memakai endpoint dan scope yang sama dengan API key; lihat referensi API dan tabel scope. Endpoint yang tidak terbuka untuk API key menjawab 403 API_KEY_NOT_ALLOWED.
  • Rate limit: 600 request per menit per instalasi. Lihat Rate limit.
  • Token bertindak sebagai anggota yang memasang aplikasi. Bila anggota itu dikeluarkan atau kehilangan owner/admin, panggilan dijawab 401 dan tenant harus mengotorisasi ulang.
GET /v1/developer/token-info
{
  "tenantId": "0192f0c0-1111-7aaa-8bbb-0123456789ab",
  "kind": "app",
  "keyId": null,
  "keyName": null,
  "appId": "0192f0e0-1b2c-7d3e-8f4a-5b6c7d8e9f0a",
  "appName": "Acme ERP Sync",
  "installationId": "0192f0e1-2c3d-7e4f-8a5b-6c7d8e9fa0b1",
  "scopes": [
    "orders:read",
    "products:read"
  ],
  "sandbox": false
}

Contoh lengkap

Aplikasi Express: redirect dengan PKCE dan state, callback, penukaran code, refresh, dan panggilan API.

import crypto from 'node:crypto';
import express from 'express';
import session from 'express-session';

const APP = 'https://app.comnethub.app';
const API = 'https://api.comnethub.app';
const CLIENT_ID = process.env.THECOMNET_CLIENT_ID; // tcn_app_…
const CLIENT_SECRET = process.env.THECOMNET_CLIENT_SECRET; // tcn_cs_…, server-side only
const REDIRECT_URI = 'https://example.com/oauth/callback'; // registered exactly
const SCOPES = 'orders:read products:read';

const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));

const b64url = (buf) => buf.toString('base64url');

// 1. Send the merchant to TheComNet with state + PKCE.
app.get('/connect', (req, res) => {
  const state = b64url(crypto.randomBytes(24));
  const verifier = b64url(crypto.randomBytes(32)); // 43 chars
  const challenge = b64url(crypto.createHash('sha256').update(verifier).digest());
  req.session.oauth = { state, verifier };
  const qs = new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    scope: SCOPES,
    state,
    code_challenge: challenge,
    code_challenge_method: 'S256',
  });
  res.redirect(`${APP}/oauth/authorize?${qs}`);
});

// Token endpoint call with HTTP Basic client auth.
async function tokenRequest(params) {
  const res = await fetch(`${API}/v1/oauth/token`, {
    method: 'POST',
    headers: {
      Authorization: 'Basic ' + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'),
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams(params),
  });
  const body = await res.json();
  if (!res.ok) throw Object.assign(new Error(body.error_description ?? body.error), { oauth: body.error });
  return body;
}

// 2. Callback: verify state, exchange the code (valid 10 min, single use).
app.get('/oauth/callback', async (req, res) => {
  const saved = req.session.oauth;
  delete req.session.oauth;
  if (!saved || req.query.state !== saved.state) return res.status(400).send('Invalid state');
  if (req.query.error) return res.status(400).send(`Not connected: ${req.query.error}`);

  const t = await tokenRequest({
    grant_type: 'authorization_code',
    code: String(req.query.code),
    redirect_uri: REDIRECT_URI,
    code_verifier: saved.verifier,
  });
  await db.saveConnection({
    tenantId: t.tenant_id,
    installationId: t.installation_id,
    accessToken: t.access_token,
    refreshToken: t.refresh_token,
    scope: t.scope,
    expiresAt: Date.now() + t.expires_in * 1000,
  });
  res.send('Connected');
});

// 3. Refresh shortly before expiry. Refresh tokens rotate: store the new pair atomically.
async function accessTokenFor(tenantId) {
  const c = await db.getConnection(tenantId);
  if (c.expiresAt - Date.now() > 60_000) return c.accessToken;
  try {
    const t = await tokenRequest({ grant_type: 'refresh_token', refresh_token: c.refreshToken });
    await db.updateTokens(tenantId, {
      accessToken: t.access_token,
      refreshToken: t.refresh_token,
      expiresAt: Date.now() + t.expires_in * 1000,
    });
    return t.access_token;
  } catch (e) {
    // Uninstalled, expired, or replayed: ask the merchant to reconnect.
    if (e.oauth === 'invalid_grant') await db.markDisconnected(tenantId);
    throw e;
  }
}

// 4. Call the API as the installation.
async function listOrders(tenantId) {
  const res = await fetch(`${API}/v1/orders?limit=50`, {
    headers: { Authorization: `Bearer ${await accessTokenFor(tenantId)}` },
  });
  if (res.status === 401) await db.markDisconnected(tenantId); // uninstalled or installer lost access
  if (!res.ok) throw new Error(`${res.status} ${(await res.json()).code}`);
  return res.json();
}

app.listen(3000);

Pencopotan

Merchant bisa mencopot aplikasi Anda kapan saja di Pengaturan → Integrasi → Aplikasi terpasang. Access token-nya langsung berhenti (401) dan refresh gagal dengan invalid_grant. Tangani keduanya dengan menandai koneksi sebagai terputus dan menawarkan sambung ulang.

Webhook aplikasi

Bila aplikasi memiliki URL webhook dan event, setiap tenant yang memasang otomatis mendapat endpoint webhook yang dikelola aplikasi.

  • Pengiriman ditandatangani dengan webhook secret aplikasi, memakai skema yang sama seperti Webhook. Satu secret mencakup semua instalasi.
  • tenantId di envelope menunjukkan instalasi pemilik event.
  • Mengubah URL atau event, atau merotasi secret, berlaku untuk semua instalasi sekaligus.
  • Tenant bisa melihat endpoint tersebut tetapi tidak bisa mengubah atau menghapusnya (WEBHOOK_MANAGED_BY_APP). Mencopot aplikasi akan menghapusnya.

Praktik terbaik

  • Simpan client secret di sisi server. Jangan pernah menyertakannya di kode browser atau mobile.
  • Buat state baru per request dan verifikasi di callback.
  • Pakai PKCE (S256) untuk setiap authorization request.
  • Refresh sesaat sebelum kedaluwarsa, satu refresh per instalasi pada satu waktu, dan simpan token hasil rotasi secara atomik.
  • Saat menerima invalid_grant atau 401, minta merchant menghubungkan ulang alih-alih mengulang request.
  • Minta scope sesedikit mungkin yang dibutuhkan aplikasi.

Uji dengan tenant Anda sendiri selama aplikasi berstatus development (sandbox mode), lalu tekan Ajukan review.