Lewati ke konten utama

Partner Access Setup — Operator Runbook

Audience: Admin / super_admin of the Kesles Merchant dashboard. Prereq: Logged in to the admin dashboard, holding role admin / super_admin / partner_admin. Architecture reference: partner-access-scope-plan.md.

This document is the operational guide for setting up merchant data access for partners: who can see which merchants, via which API, and how to disconnect/revoke access when the contract ends.

1. Quick Concepts

Partner in Kesles = an external entity that has read access to merchant data via the /api/partner/v1/* API. Each partner has:

  1. Identity — a row in partner.partners (name, type, status).
  2. Access Scope — visibility mode: which merchants may be seen.
  3. API Credentials — key/secret pair for API auth.

These three are configured independently. Admins can create the identity without immediately granting credentials (e.g. while still negotiating a contract), and can change the access scope at any time without regenerating credentials.


2. Decision Matrix — Access Scope Mode

Quick guide for picking the right mode based on the type of partnership.

Partnership with…Partner TypeAccess Scope Modeaccess_partner_ids
Regular sales/reseller acquiring merchantssales, reseller, agency, affiliate, communityown_referralempty
Bank / lender during merchant underwritingbank, finance_lenderexplicitempty — granted manually per merchant
3rd-party scoring bureauscoring_bureauexplicitempty
VC/angel investing in 1 partner (Joob ecosystem only)venture_capitalpartner_network{<Joob uuid>}
Holding shareholder owning multi partner (A+B+C)shareholderpartner_network{<uuid A>, <uuid B>, <uuid C>}
Joint fee-share gateway across 2 ecosystemsagency / otherpartner_network{<uuid A>, <uuid B>}
Regulator (OJK/BI), Kesles shareholder, internal BIregulator, shareholder, otherglobal (super_admin only)

When in doubt: default to own_referral + empty list. The partner will not see anything until the admin actually sets a scope — this is safe, not a bug. Fail-closed.


3. Runbook 1 — Onboarding a New Partner

Scenario: Kesles signed an MoU with a new partner; now it needs to be configured in the dashboard.

Step 1 — Create the Partner Record

  1. Log in to the admin dashboard.
  2. Sidebar → Master Data → Partners.
  3. Click + Add Partner.
  4. Fill in the form:
    • Partner Code — auto-generated (PRT-<random hex>), no need to fill in.
    • Partner Name — display name (e.g. "PT Joob Global").
    • Legal Name — official name per legal documents.
    • Partner Type — pick per the Matrix §2.
    • Entity Typecompany / individual / organization / community.
    • Statusactive.
    • Contact Person, Phone, Email — PIC contact for technical communication.
    • Referral Code — fill in only if the partner recruits merchants; leave empty for viewers (VC, regulator, bank).
    • Notes — MoU summary: contract number, date, scope expectations.
  5. Save Partner.

Result: a new row in partner.partners, default access_scope_mode='own_referral', access_partner_ids='{}'. This partner cannot hit the API yet because there are no credentials.

Step 2 — Set Access Scope

On the Partners page, find the newly created partner → click the shield icon 🛡️ in the Action column → the Access Control dialog appears.

In the dialog:

  1. Pick the mode per matrix §2.
    • own_referral — regular sales partner, no extra configuration needed.
    • explicit — bank/lender, no configuration needed in this dialog; grants are filled in manually in the partner.merchant_access table when consent is signed.
    • partner_network — a checkbox list of all other partners appears. Tick the source partners this viewer is allowed to see.
    • global — only available for super_admin. There is a red warning + double confirmation.
  2. Click Save.
  3. A confirmation modal appears with a Reason field (mandatory when the mode changes or when set to global).
  4. Fill in an informative reason for audit, e.g.:
    • "Akbar Investor in Joob Global per MoU 2026-02-14"
    • "Joint fee-share gateway with PT A and PT B per contract 2026-Q2-007"
    • "OJK access for the 2026 annual audit per SE OJK 14/2026"
  5. Save again → a row is recorded in partner.access_scope_changes (permanent audit trail).

Step 3 — Generate API Credential

In the same panel (Partners page), open the partner detail → tab API Credentials → click + Create Credential.

Modal:

  • Label — name for internal identification (e.g. "Akbar — production", "Joob CRM integration"). Not exposed to the partner.
  • Scopes — tick what is needed:
    • merchant.read — list of merchants + identity details.
    • transaction.read — transactions per merchant.
    • scoring.read — business scoring summary.
    • risk-summary.read — alternative scoring (aliased).
  • Expires — pick:
    • Never — permanent credential (not recommended for external partners).
    • 30 days / 90 days / 1 year — rotation required before that date.
  • Click Create.

Reveal-once modal appears:

⚠️ Credential Secret — save now

credential_key: akb_pk_2026_a1b2c3
credential_secret: kls_sk_9f4e2a7d8b1c6e3f5a0b8d2c4e6f1a3b [📋 Copy]

Send to the partner via a secure channel.

IMPORTANT:

  • The secret cannot be viewed again after the modal is closed. Only the hash is stored in the DB.
  • If you forget to save it / don't copy in time → must revoke + create again.
  • Send it to the partner via a secure channel: encrypted email, signed PDF, or a shared password manager. NOT via public chat / plain email.

Step 4 — Partner Verification

Ask the partner to test from their side:

# Step 1 — exchange credential for token
curl -X POST https://api.kesles.example/api/partner/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"credential_key": "akb_pk_2026_a1b2c3",
"credential_secret": "kls_sk_9f4e..."}'

# Step 2 — hit portfolio endpoint
curl https://api.kesles.example/api/partner/v1/partners/<partner_id>/portfolio \
-H "Authorization: Bearer <access_token_from_step_1>"

Expected:

  • Step 1 returns access_token + expires_in.
  • Step 2 returns JSON with the number of merchants matching the configured scope.

If the partner reports "got no merchants at all" → see Troubleshooting §9.


4. Runbook 2 — Change Access Scope of an Existing Partner

Scenario: An existing partner needs additional visibility (e.g. Akbar now also invests in partner B, needs to be added to their network).

  1. Master Data → Partners → find the partner → click shield 🛡️.
  2. The dialog opens with the current mode + list.
  3. Tick the additional partner in the list (or untick those to revoke).
  4. Save → enter a new reason, e.g. "Add partner B per MoU addendum 2026-04-22".
  5. New audit row in partner.access_scope_changes with before/after.

Immediate effect: the next partner request immediately applies the new scope. No restart required.

To downgrade from global to partner_network: Same. Fill in reason "revocation of global access per contract expiry". Super_admin is not required — even a regular admin can drop to a stricter mode.

To upgrade to global: Super_admin only. The confirmation modal will warn in red. The server log will emit partner_access_scope.global_enabled for SRE alerting.


5. Runbook 3 — Revoking Partner Access

Scenario: The partnership has ended; the partner must lose access.

  1. Master Data → Partners → edit the partner → Status = inactive or suspended → Save.
  2. Backend rejects all token issuance for this partner's credential (401 Unauthorized).

Option B — Revoke credential (if you want the partner identity to remain active but API access revoked)

  1. Master Data → Partners → partner detail → tab API Credentials.
  2. Click Revoke on the active credential → confirm.
  3. The credential status is set to revoked in the DB; the credential cannot be used to obtain a new token.
  4. Existing tokens remain valid until they expire (max 1 hour) — by design stateless JWT.

Option C — Downgrade scope without revoke

  1. Open the Access Control dialog.
  2. If mode partner_network → uncheck all partners; mode becomes partner_network with an empty list → backend fails closed, the partner gets 0 merchants even though the credential is still active.
  3. Or switch to explicit without granting any merchants.

Which to use?

  • Contract expired / terminated → Option A.
  • Credential rotation (security hygiene) → Option B, then issue a new credential.
  • Temporary pause (e.g. active dispute) → Option C.

6. Credential vs Token — Important Note

These are often confused. Summary:

CredentialToken
LifetimeMonthly / yearly~1 hour
Created byKesles admin (dashboard)Partner (hits /auth/token)
FunctionPermanent identityTemporary access ticket
Stored by partnerSecret managerIn memory only
If leakedMust revoke immediatelyWait for expiry (≤1 hour)
How to revokeSet status = revoked in DBNot needed — expires on its own

See the full explanation in partner-access-scope-plan.md §6.


7. Audit — Who Changed What

Every access-scope change is permanently recorded in partner.access_scope_changes — minimum retention 5 years (compliance).

Check the last 10 changes for 1 partner

SELECT
created_at,
actor_role,
changed_by_user_id,
before_mode,
after_mode,
before_partner_ids,
after_partner_ids,
reason,
ip_address
FROM partner.access_scope_changes
WHERE partner_id = '<uuid>'
ORDER BY created_at DESC
LIMIT 10;

Check who has ever been set to global

SELECT
c.created_at,
p.partner_code, p.partner_name,
c.actor_role, c.changed_by_user_id,
c.reason
FROM partner.access_scope_changes c
JOIN partner.partners p ON p.id = c.partner_id
WHERE c.after_mode = 'global'
ORDER BY c.created_at DESC;

Check out-of-scope requests (indicator of credential theft)

SELECT
created_at, partner_id, request_path,
response_status, access_scope_mode
FROM partner.api_audit_logs
WHERE scope_violation = true
AND created_at >= now() - interval '7 days'
ORDER BY created_at DESC;

8. Credential Rotation (Security Hygiene)

Recommendation: rotate credentials every 90 days for active partners.

Flow:

  1. Notify the partner 7 days in advance.
  2. Generate a new credential via the Create Credential button (step 3 §3).
  3. Send to the partner via a secure channel.
  4. Partner switches their tooling to the new credential.
  5. Once the partner confirms the switch → Revoke the old credential.

Do not revoke before the partner switches — an overlap window is required.


9. Troubleshooting

"Partner says the API returns 0 merchants"

Step-by-step diagnosis:

  1. Credential active?

    SELECT id, credential_key, status, expires_at
    FROM partner.api_credentials
    WHERE partner_id = '<uuid>';

    If status = 'revoked' or expires_at < now() → credential is dead.

  2. Check the partner's access scope?

    SELECT access_scope_mode, access_partner_ids
    FROM partner.partners
    WHERE id = '<uuid>';
    • If mode is partner_network but access_partner_ids is empty {} → fail-closed, returns 0 merchants. Fix: open Access Control, tick the relevant partners.
    • If mode is own_referral but this partner truly has no referred merchants → returning 0 is correct.
  3. Check whether any merchant matches the filter?

    -- For mode own_referral
    SELECT COUNT(*) FROM merchant.merchants
    WHERE referral_partner_id = '<uuid>' AND deleted_at IS NULL;

    -- For mode partner_network
    SELECT COUNT(*) FROM merchant.merchants
    WHERE referral_partner_id = ANY(
    (SELECT access_partner_ids FROM partner.partners WHERE id = '<uuid>')
    )
    AND deleted_at IS NULL;
  4. Check the partner's most recent request audit log:

    SELECT created_at, request_path, access_scope_mode,
    merchant_scope_count, response_status, scope_violation
    FROM partner.api_audit_logs
    WHERE partner_id = '<uuid>'
    ORDER BY created_at DESC
    LIMIT 5;

"Partner says response is 403 Forbidden"

The partner hit an endpoint for a merchant outside their scope. This is by design — the ScopeResolver helper filters at the query level, and the requested merchant does not match → 403 + scope_violation=true in the audit.

Action: No fix needed if it is genuinely out of scope. If it really should be allowed → add the merchant to the scope:

  • Mode own_referral / partner_network: add the merchant via partner.merchant_access grants, or make sure the merchant's referral_partner_id is correct.
  • Mode explicit: add a grant in partner.merchant_access.

"Partner says response is 401 Unauthorized"

Bearer token expired or invalid. The partner must hit POST /api/partner/v1/auth/token again with their credential to get a new token.

If the credential is correct and it's still 401 → check the partner's status (might be suspended):

SELECT status, deleted_at FROM partner.partners WHERE id = '<uuid>';

"Admin cannot change the mode to global"

Mode global can only be set by the super_admin role. For regular admins → the global option is disabled in the dialog.

If genuinely needed → escalate to a super_admin or have the super_admin perform the change.


10. Core Steps Summary

1. Add Partner → Master Data → Partners → + Add Partner
2. Set Access Scope → click shield 🛡️ → pick mode → Save + reason
3. Create Credential → tab API Credentials → + Create Credential → copy secret
4. Send to partner → secure channel
5. Partner verifies → curl /auth/token + /portfolio
6. Monitor → audit log + SRE alerts for mode 'global'
7. Rotate → 90 days: create new, send, revoke old
8. Terminate → suspend partner or revoke credential

11. Revenue Share — Fee Type & Basis

The Revenue Share panel (🟩 percent icon in the partner row) has 2 fields that often confuse: Fee Type and Basis. They combine, they are not alternatives.

  • Fee Type = the kind of money being shared. Answers "this fee comes from what transaction?"
  • Basis = the way it is computed. Answers "computed based on what?"

11.1 Fee Type — where the money comes from

Fee TypeMoney being sharedWhen to use
qris_mdrA cut of the MDR (Merchant Discount Rate) from each merchant QRIS transactionFee-share with an acquirer / gateway / acquisition partner. The most common one.
service_feeDaily/monthly service fee charged to the merchant (e.g. Rp 2,000/day)Partners receiving commission from device subscription fees
subscriptionMonthly SaaS/software subscription feeIf there is an additional subscription product outside QRIS
settlement_feeFee per settlement (payout to the merchant's account)Partners processing settlement payouts
onboarding_feeOne-time fee paid when a merchant onboardsSales partners getting an activation commission (e.g. Rp 100,000 per new merchant)
otherCustom — use the notes field to explainCatchall for non-standard agreements

11.2 Basis — how it is computed

BasisFormulaRequired inputExample
gross_amountgross × rate%rate_bps (percent)Partner gets 0.3% of merchant QRIS gross
net_amount(gross − MDR) × rate%rate_bps (percent)Partner gets 5% of net amount (after MDR is taken)
transaction_counttx_count × nominalfixed_amount (IDR)Partner gets Rp 100 per successful transaction
fixed_per_merchantactive_merchant_count × nominalfixed_amount (IDR/month)Partner gets Rp 50,000/month per active merchant
fixed_per_monthnominal (flat, independent of volume)fixed_amount (IDR/month)Partner gets a Rp 5,000,000/month retainer

11.3 Reasonable Combinations

Business caseFee TypeBasisInput
Sales partner gets 0.3% of every QRIS from their recruited merchantqris_mdrgross_amountrate_bps = 30 (= 0.30%)
Bank partner gets 5 bps of net (after MDR) for settlementsettlement_feenet_amountrate_bps = 5 (= 0.05%)
Community leader gets Rp 100 per successful transactionqris_mdrtransaction_countfixed_amount = 100
Acquirer gets Rp 10,000/month per active merchantservice_feefixed_per_merchantfixed_amount = 10000
Rp 5 million/month consulting retainerotherfixed_per_monthfixed_amount = 5_000_000
Onboarding commission Rp 150,000 per new merchantonboarding_feefixed_per_merchantfixed_amount = 150000

11.4 Unit Notes

  • rate_bps in DB is stored in basis points: 100 bps = 1%. The UI converts — operators enter "0.50" in the form, the backend stores 50.
  • fixed_amount in DB: integer IDR. Type "50000" = Rp 50,000.
  • The form validator rejects invalid combinations (e.g. percent basis but fixed_amount filled → 422 from backend, the form shows "basis gross_amount requires rate_bps").

11.5 Effective From / Effective To

  • Effective From is mandatory — the date the rule starts being effective.

  • Effective To is optional — if empty, the rule applies indefinitely.

  • Changing the rate mid-stream: do not edit an active row. The correct pattern:

    1. Edit the old row → set Effective To to the day before the change date.
    2. Add a new Rule → Effective From set to the change date, with the new rate.

    The old row remains as a reference for reconciliation of the period before the change.

11.6 Source Partner

  • Empty (— Across all visible sources —): rule applies to all merchants visible to this partner (per Access Scope). Suitable for global partners like the gateway acquirer.
  • Pick 1 specific partner: rule only applies to merchants referred by that partner. Suitable for joint ecosystem fee-share with a different rate per ecosystem.

Example: Partner C fee-shares with Partner A (3 bps) and Partner B (5 bps) → create 2 rules:

  • Rule 1: source = A, rate = 3 bps
  • Rule 2: source = B, rate = 5 bps

  • Partner Access Scope Plan — architecture + decision log + ship progress.
  • Partner API Reference — endpoint spec to share with the partner.
  • DB Network Topology — VPN access to the DB for debugging audit logs.