API Developer BerKarya

Untuk sistem lain yang membaca atau menulis data kepegawaian sebuah perusahaan di BerKarya: ERP, aplikasi kasir, sistem properti, akuntansi, atau dasbor internal.

Basis URL: https://berkarya.app/api/partner/v1

1. Mulai cepat

  1. HR perusahaan membuka /admin/integrasi di BerKarya dan menerbitkan kunci dengan scope yang dibutuhkan. Belum punya perusahaan? Daftarkan di sini.
  2. Simpan kuncinya di server Anda. Kunci hanya ditampilkan sekali.
  3. Panggil /ping untuk memastikan kunci, perusahaan, dan izinnya.
curl -s https://berkarya.app/api/partner/v1/ping \
  -H "Authorization: Bearer $BERKARYA_KEY"

⚠️ Kunci API adalah rahasia server. Jangan menaruhnya di aplikasi seluler, kode peramban, atau variabel lingkungan yang ikut terkirim ke klien.

2. Kunci dan scope

Authorization: Bearer bk_live_<12 karakter>_<32 karakter>

Server hanya menyimpan sidik SHA-256 kunci. Kunci hilang? Terbitkan yang baru lalu cabut yang lama. Kunci dicabut, tidak dihapus, supaya jejak audit tetap menunjuk ke pemiliknya. Izin sebuah kunci bisa diubah HR kapan saja tanpa mengganti kuncinya; perubahan (termasuk pencabutan izin) berlaku di permintaan berikutnya.

Scope tidak punya wildcard dan tidak punya hierarki: employees:write tidak memberi employees:read. Kunci hanya bisa apa yang tertulis.

ScopeArtinya
org:readMembaca data organisasi: kantor, jabatan, shift, dan hari libur.
employees:readMembaca daftar karyawan beserta jabatan, kantor, dan statusnya.
employees:writeMembuat dan memperbarui data karyawan. Tidak bisa menghapus.
attendance:readMembaca catatan kehadiran: jam masuk, jam pulang, status, dan tanda peninjauan.
roster:readMembaca jadwal shift yang sudah ditetapkan.
requests:readMembaca pengajuan cuti, sakit, izin, lembur, dan reimburse beserta keputusannya.
payroll:readMembaca slip gaji yang SUDAH diterbitkan. Draf tidak pernah ikut.
points:readMembaca poin karyawan dan aturannya.
points:writeMemberi poin kepada karyawan. Dipakai modul aktivasi seperti Berkomunitas.
webhooks:manageMendaftarkan dan mencabut alamat webhook milik perusahaan ini.
companies:adminMembuat perusahaan baru dan bertindak atas perusahaan mana pun. HANYA untuk kunci platform.(kunci platform)

Kunci platform (tanpa perusahaan, diterbitkan oleh pengelola BerKarya) wajib menyebut perusahaan tujuan di setiap permintaan per-perusahaan lewat header X-BerKarya-Company: <id atau slug>. Tanpa header itu permintaannya ditolak, bukan dijawab dengan gabungan data semua perusahaan.

3. Bentuk respons

// sukses berhalaman
{ "ok": true, "data": [ … ], "nextCursor": "clx…" }

// galat
{ "ok": false, "error": "kalimat untuk manusia", "code": "SCOPE_DENIED" }

Bercabanglah pada code, bukan pada error. Kalimatnya boleh berubah kapan saja tanpa dianggap perubahan kontrak.

codeHTTPArtinya
UNAUTHORIZED401Header Authorization tidak ada atau bukan Bearer.
KEY_INVALID401Kunci salah, dicabut, atau kedaluwarsa.
SCOPE_DENIED403Kunci sah, tapi izinnya kurang. Field butuh menyebut scope yang diperlukan.
COMPANY_REQUIRED400Kunci platform tanpa header X-BerKarya-Company.
RATE_LIMITED429Melewati batas laju. Ada retryAfterSec dan header Retry-After.
VALIDATION400Bentuk permintaan salah.
NOT_FOUND404Tidak ada, atau milik perusahaan lain (sengaja tidak dibedakan).
DUPLICATE409Sudah ada dan tidak boleh dibuat dua kali.
CONFLICT409Keadaan sumber daya menolak aksi ini.
SERVER_ERROR500Kesalahan di sisi kami.
  • Paginasi: ?limit= (bawaan 50, maksimum 200) dan ?cursor=. Berhenti saat nextCursor bernilai null, bukan saat jumlah data lebih kecil dari limit.
  • Uang (gross, deductions, net, amount) dikirim sebagai string desimal, misalnya "1250000.00". Jangan parse sebagai float.
  • Tanggal kerja berbentuk YYYY-MM-DD tanpa zona. Waktu kejadian berbentuk ISO-8601 UTC.
  • Sinkronisasi bertahap: endpoint yang menerima updatedSince hanya mengirim baris yang berubah sejak waktu itu.

4. Endpoint

MetodeJalurScopeKegunaan
GET/ping—Identitas kunci: perusahaan mana, izin apa.
GET/orgorg:readKantor, jabatan, shift, dan hari libur sekaligus.
GET/employeesemployees:readDaftar karyawan, berhalaman.
POST/employeesemployees:writeBuat atau perbarui karyawan (upsert lewat externalId).
GET/employees/{id}employees:readSatu karyawan. {id} boleh id BerKarya atau externalId Anda.
GET/attendanceattendance:readCatatan kehadiran per tanggal kerja.
GET/rosterroster:readJadwal shift yang sudah ditetapkan.
GET/requestsrequests:readPengajuan cuti, sakit, izin, lembur, reimburse beserta keputusannya.
GET/payrollpayroll:readSlip gaji yang SUDAH diterbitkan. Draf tidak pernah ikut.
GET/pointspoints:readPoin karyawan.
POST/pointspoints:writeBeri poin kepada karyawan (idempoten).
GET/webhookswebhooks:manageDaftar alamat webhook milik perusahaan ini.
POST/webhookswebhooks:manageDaftarkan alamat webhook. Rahasia penanda tangan ditampilkan sekali.
PATCH/webhooks/{id}webhooks:manageNyalakan/matikan alamat atau ubah langganan event.
DELETE/webhooks/{id}webhooks:manageCabut alamat webhook.
GET/companiescompanies:adminDaftar perusahaan. Kunci platform saja.
POST/companiescompanies:adminBuat perusahaan baru. Kunci platform saja.

GET/ping

Identitas kunci: perusahaan mana, izin apa.

Scope: tidak ada

Panggil ini pertama kali. Membedakan "kunci salah" (401) dari "izin kurang" (403) sebelum menebak-nebak.

{
  "ok": true,
  "key": { "id": "clk…", "name": "Livioo produksi", "platform": false },
  "scopes": ["org:read", "employees:read", "attendance:read"],
  "company": { "id": "clc…", "name": "PT Contoh", "slug": "pt-contoh", "timezone": "Asia/Jakarta" },
  "serverTime": "2026-10-03T01:00:00.000Z"
}

GET/org

Kantor, jabatan, shift, dan hari libur sekaligus.

Scope: org:read

GET/employees

Daftar karyawan, berhalaman.

Scope: employees:read · Parameter: status, updatedSince, limit, cursor

{
  "ok": true,
  "data": [
    {
      "id": "cle…",
      "externalId": "LIV-882",
      "nip": "0012",
      "name": "Siti Rahma",
      "email": null,
      "phone": "+6281234567890",
      "area": null,
      "status": "active",
      "joinDate": "2026-03-01",
      "managerId": null,
      "position": { "id": "clp…", "name": "Front Office" },
      "office": { "id": "clo…", "name": "Kantor Pusat" },
      "createdAt": "2026-03-01T02:00:00.000Z",
      "updatedAt": "2026-09-20T08:12:00.000Z"
    }
  ],
  "nextCursor": null
}

POST/employees

Buat atau perbarui karyawan (upsert lewat externalId).

Scope: employees:write

Dijalankan dua kali menghasilkan satu karyawan. Field yang tidak dikirim tidak disentuh; null eksplisit mengosongkan.

curl -s -X POST https://berkarya.app/api/partner/v1/employees \
  -H "Authorization: Bearer $BERKARYA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "externalId": "LIV-882",
  "name": "Siti Rahma",
  "phone": "+6281234567890",
  "position": "Front Office",
  "joinDate": "2026-03-01"
}'

GET/employees/{id}

Satu karyawan. {id} boleh id BerKarya atau externalId Anda.

Scope: employees:read

GET/attendance

Catatan kehadiran per tanggal kerja.

Scope: attendance:read · Parameter: from, to, employeeId, updatedSince, limit, cursor

Koordinat mentah dan foto tidak dikirim: yang ada jarak ke kantor dan hasSelfie.

{
  "ok": true,
  "data": [
    {
      "id": "cla…",
      "externalId": null,
      "employeeId": "cle…",
      "date": "2026-10-03",
      "checkIn": {
        "at": "2026-10-03T01:04:00.000Z",
        "status": "LATE",
        "lateMin": 4,
        "officeId": "clo…",
        "distanceM": 12,
        "accuracyM": 8,
        "hasSelfie": true,
        "mocked": false
      },
      "checkOut": null,
      "workedMin": null,
      "note": null,
      "flags": [],
      "reviewState": "AUTO_OK",
      "reviewedAt": null,
      "reviewNote": null,
      "updatedAt": "2026-10-03T01:04:00.000Z"
    }
  ],
  "nextCursor": null
}

GET/roster

Jadwal shift yang sudah ditetapkan.

Scope: roster:read · Parameter: from (wajib), to (wajib), employeeId

GET/requests

Pengajuan cuti, sakit, izin, lembur, reimburse beserta keputusannya.

Scope: requests:read · Parameter: status, type, employeeId, updatedSince, limit, cursor

GET/payroll

Slip gaji yang SUDAH diterbitkan. Draf tidak pernah ikut.

Scope: payroll:read · Parameter: period (YYYY-MM), employeeId

gross, deductions, net dikirim sebagai string desimal.

GET/points

Poin karyawan.

Scope: points:read · Parameter: employeeId

POST/points

Beri poin kepada karyawan (idempoten).

Scope: points:write

Kirim ulang dengan idempotencyKey yang sama: poin tidak ditambah dua kali, balasannya baris yang sama dengan "duplicate": true.

curl -s -X POST https://berkarya.app/api/partner/v1/points \
  -H "Authorization: Bearer $BERKARYA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "employee": "LIV-882",
  "points": 25,
  "reason": "Menyelesaikan misi Berkomunitas",
  "idempotencyKey": "berkomunitas-misi-9912"
}'

GET/webhooks

Daftar alamat webhook milik perusahaan ini.

Scope: webhooks:manage

POST/webhooks

Daftarkan alamat webhook. Rahasia penanda tangan ditampilkan sekali.

Scope: webhooks:manage · Parameter: url (https), events (kosong = semua, termasuk event baru), description

Hanya https. Alamat lokal dan jaringan privat ditolak. Simpan secret dari balasan; tidak bisa dibaca lagi.

curl -s -X POST https://berkarya.app/api/partner/v1/webhooks \
  -H "Authorization: Bearer $BERKARYA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://contoh.id/hooks/berkarya",
  "events": ["attendance.punched", "request.decided"],
  "description": "Sinkron absensi ke ERP"
}'

PATCH/webhooks/{id}

Nyalakan/matikan alamat atau ubah langganan event.

Scope: webhooks:manage · Parameter: active, events, description

DELETE/webhooks/{id}

Cabut alamat webhook.

Scope: webhooks:manage

GET/companies

Daftar perusahaan. Kunci platform saja.

Scope: companies:admin

POST/companies

Buat perusahaan baru. Kunci platform saja.

Scope: companies:admin

5. Data yang sengaja tidak dikirim

  • Koordinat mentah absen. Yang dikirim jarak ke kantor.
  • Foto selfie. Hanya hasSelfie: true|false.
  • Lampiran pengajuan. Isinya surat dokter dan nota pribadi.
  • Slip gaji berstatus draf. Belum final dan belum dilihat karyawannya.

Butuh salah satunya untuk alasan yang sah? Ajukan scope baru. Bentuk yang ada tidak akan dilebarkan diam-diam.

6. Webhook

Daftarkan alamat https lewat POST /webhooks atau di /admin/integrasi, lalu BerKarya menghubunginya saat sesuatu terjadi.

EventKapan
attendance.punchedKaryawan absen masuk atau pulang. Memuat status, tanda peninjauan, dan jarak ke kantor.
attendance.reviewedAtasan mengesahkan atau menolak absen yang ditandai.
request.submittedPengajuan baru masuk (cuti, sakit, izin, lembur, reimburse).
request.decidedPengajuan disetujui atau ditolak.
payroll.publishedSlip gaji satu periode diterbitkan ke karyawan.
employee.createdKaryawan baru dibuat.
employee.updatedData karyawan berubah.
points.grantedPoin diberikan kepada karyawan.
POST https://alamat-anda/…
X-BerKarya-Event: attendance.punched
X-BerKarya-Delivery: wd_…
X-BerKarya-Signature: t=1758503011,v1=9f2a…

{
  "id": "wd_…",
  "event": "attendance.punched",
  "createdAt": "2026-09-22T01:03:11.000Z",
  "data": { … }
}

v1 adalah HMAC-SHA256 dari "<t>.<badan mentah>" dengan rahasia alamat Anda. Contoh verifikasi (Node.js):

import { createHmac, timingSafeEqual } from "node:crypto";

export function sah(secret, badanMentah, header, toleransiDetik = 300) {
  const bagian = Object.fromEntries(
    header.split(",").map((p) => p.split("=").map((x) => x.trim())),
  );
  const t = Number(bagian.t);
  if (!Number.isFinite(t)) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleransiDetik) return false;

  const harap = createHmac("sha256", secret).update(`${t}.${badanMentah}`).digest();
  const dapat = Buffer.from(bagian.v1, "hex");
  return harap.length === dapat.length && timingSafeEqual(harap, dapat);
}
  • Pakai badan mentah, bukan hasil parse lalu stringify ulang.
  • Periksa stempel waktu supaya muatan lama tidak bisa diputar ulang.
  • Bandingkan dengan timingSafeEqual, bukan ===.
  • Balas 2xx dalam 10 detik. Gagal akan dicoba lagi setelah 1, 5, 30, 120, dan 360 menit. Pengiriman bersifat setidaknya sekali, jadi penerima wajib idempoten terhadap id.

7. Batas laju dan jejak

Batas laju dihitung per kunci per menit (bawaan 120, diatur saat menerbitkan). Lewat batas dijawab 429 RATE_LIMITED beserta retryAfterSec.

Setiap penulisan lewat kunci tercatat di jejak audit perusahaan dengan nama kuncinya, sehingga HR selalu bisa melihat sistem mana yang mengubah apa.

Pertanyaan integrasi atau permintaan scope baru: wiro@drwcorp.com.