think
16px
820px

Peruri Sign-IT integration — PERISAI e-Signature + TERA e-Stamp

Status 2026-07-17: staging credentials received and token-verified; the TERA product
slice is BUILT and e2e-proven against the mock provider; all corp-scoped staging calls
are blocked on a Peruri-side client↔corporate binding (see "Staging findings").

Source spec: docs/PERURI/E-SIGNATURE/ (NDA — gitignored; keep payload samples out of this file).

LIVE STAGING RESULTS — binding activated 2026-07-21

Peruri activated the clientId↔PDS-CLIENT binding. Re-tested end-to-end against real
staging. Everything works up to the human/MFA boundary:

  • ✅ Token, province/city references.
  • ✅ TERA user register / registry / deregister — after live-found fixes: the gateway
    wants the email as an array (string body 43-errors), and per-user checks must use
    /tera/.../user/data (00 registered / 04 not); /user/all ignores its email
    filter and an empty registry answers 25.
  • ✅ PERISAI form-user registration through our wizard (not_registered → kyc_required).
  • ✅ eKYC video upload — pipeline works; Peruri's face check correctly rejects a
    synthetic clip (1006 "Verifikasi E-KYC ditolak, percobaan ke-1 dari 3"). A real face
    is required to pass — cannot be automated.
  • ✅ TERA/PERISAI document send — real orders created (e.g. TERA orderId 728650).
  • BLOCKER #2 RESOLVED (Peruri answer, 2026-07-21): the MFA is KEYLA. For TERA
    stamping use the same single/signing endpoint with otpCode empty and the
    signer's 8-digit KEYLA-app token in token (+ orderId, signDescription).
    KEYLA implementation is mandatory ("implementasi Keyla adalah wajib"); an Email-OTP
    option can be requested from Peruri separately. Implemented as SIGNIT_MFA=keyla
    (default) | otp; in KEYLA mode the deprecated GET /otp/.../sign is never called.
    Live-verified: the demo ceremony now reaches 53 UserTidakTerdaftarKEYLA — i.e. the
    full client pipeline is correct, and the last step is the signer enrolling in the
    KEYLA app (real-person step; ask Peruri how staging test users enroll).
  • ⚠️ Coordinate gotchas (both live-proven, both fixed):
    1. The signer array must live inside payload (Postman V1.5). A top-level
    signer is accepted at send but silently discarded → signing fails
    47 No Signature Coordinate Found.
    2. varLocation must be present and non-empty — omitting it also drops the
    visual coordinate (same 47). The adapter now defaults it ("Indonesia") and never
    omits it.
  • Fallback that also works: PUT /specimen/{v}/{corp}/coordinate/signature sets the
    coordinate on an existing order explicitly (returns 00).
  • Error mapping added: 53 → keyla_unenrolled, 54/55 → keyla_token_invalid (both
    actionable messages in the ceremony modal); FE OTP modal is KEYLA-aware (8-digit
    token prompt, no resend button).

Staging findings (first live contact, 2026-07-17)

  • The credential file is CRLF — strip \r or Basic auth fails with 82.
  • GET /auth/v1/token/generate works: envelope 00, daily token expiring 23:59:59 WIB.
    Staging gateway self-reports v24.11.28 (prod reports v26.02.26); both are newer
    than the V1.4 spec, so expect drift — e.g. gateway messages not in the spec's error
    table.
  • Blocker: every corp-scoped endpoint under PDS-CLIENT returns
    03 "User Tidak Diperkenankan" (not the spec's meaning of 03). Discriminator: an
    unknown corporate returns 02 "Sistem ID tidak terdaftar", so PDS-CLIENT is a real
    system id and our clientId simply isn't authorized for it yet. Peruri must activate
    the binding (or give the corporateId actually assigned to us) + provision cert/TERA
    quota. Prod rejects the staging creds (as expected).
  • The V1.5 Postman collection (ADSG-Client) has no OTP folder, while its signing
    bodies still carry {orderId, otpCode, token} and the FE compliance checklist words
    the signing/stamping MFA as an 8-digit KEYLA token. Spec section 2.3.1 confirms
    the client "receives the Token from the OTP request" — i.e. our RequestOTP→token
    cache design is right for the OTP flow; ask Peruri whether our corporate is
    provisioned for SMS/email OTP, KEYLA, or both.
  • Sign-IT remains cloud-only: the 116-page spec has no on-premise deployment mode.
    The on-prem Docker "Sign Adapter" module in docs/PERURI/ is part of the e-METERAI
    solution (hash-only egress), which we already run. For PERISAI/TERA the document
    travels to Peruri as base64 (≤5MB) by design — Peruri holds the signing keys (PSrE);
    our integration stays on-prem-friendly (pure outbound REST, no callbacks).

TERA product slice (built 2026-07-17, worktree branch)

POST /api/v1/documents/{docID}/versions/{version}/stamp (OTP ceremony via the shared
/esign/seals/{jobID} endpoints) + GET .../stamps; stamp_records ledger with the
meterai-style claim-before-charge one-stamp-per-document guard; stamp.affix
capability (cap-stamp-affix, position toggle, can_affix_stamp ability); versions
land as status stamped; Signatures-tab StampSection with drag-drop placement +
stamp-flavored OTP modal; Admin → Security TERA user card (check/register/deregister).
OTP submit/resend now route by the pending seal's KIND, fixing the split-provider case.
Proven end-to-end (API + scripted browser) against STAMP_PROVIDER=mock +
ESIGN_MOCK_OTP=true; flipping to staging is config-only once Peruri unblocks.

Ceremony locality (locked design): every ceremony happens inside Obscura — send,
OTP entry, download are all server-to-server API calls; there is no redirect to a
Peruri page, no embedded provider widget, and the Sign-IT adapter never returns a
RedirectURL. The registration/eKYC journey will follow the same rule (the liveness
video is captured in our UI and uploaded once via API — never streamed, never
browser→Peruri).

Workflow digital-stamp step (built 2026-07-18): the designer/builder offers a
stamp step kind routed as an org duty to stamp.affix holders (same override rule
as meterai). Because TERA seals the document, ValidateSteps enforces the stamp step
is the LAST step of any definition (authoring, revision, custom start and save all
reject a non-final stamp step); the affix hook — sync handler or async seal
completion — advances/completes the instance. Proven e2e: approve → stamp task →
OTP affix → instance approved with a stamp transition. Also fixed: the start-time
affix-permission guard now considers named assignees, not only position holders
(previously a named-only meterai step was always rejected at start).

Sign-IT is Peruri's cloud gateway for two products we are contracting:

  • PERISAI — certified digital signature backed by a per-user Peruri certificate
    (Komdigi-registered PSrE). This becomes the Official signature tier when
    ESIGN_PROVIDER=signit.
  • TERA — digital stamp (stempel elektronik) generated from the PERURI Seal. This is a
    NEW seal kind (stamp) — distinct from both the certified signature and the e-Meterai
    tax stamp (which stays on the on-prem sign-adapter via METERAI_PROVIDER=peruri).

Gateway shape (what the adapter encodes)

  • Base URL: staging https://apistg.sign-it.id/ (prod URL comes with the contract).
  • Auth: GET /auth/v1/token/generate with Basic base64(clientId:clientSecret)
    bearer accessToken valid until 23:59:59 WIB that day. The adapter caches it and
    re-generates on expiry/401-class codes.
  • Every response uses one envelope: {status, message, data?, version, time};
    status=="00" is success, anything else maps through the documented error-code list
    (99 codes). The adapter maps the operationally meaningful ones onto kernel.Error
    kinds (wrong OTP → validation, quota → conflict, doc >5MB → validation, expired →
    terminal failure, token problems → retry-with-fresh-token).
  • Documents travel as base64 PDF ≤ 5MB, orders expire after 3 days unsigned,
    coordinates are the same bottom-left lowerLeft/upperRight + page rect our placement
    editor already produces.

Mapping onto the existing esign context

No new context. Sign-IT is one more ExternalSealer adapter (sealer_signit.go), the
same seam Mekari and the Peruri e-Meterai adapter plug into:

Sign-IT flow Obscura port Notes
PERISAI single send → signing(otp) → download Seal(SealSign)SealAwaitingOTP, ChallengeSealer.RequestOTP/SubmitOTP, AsyncSealer.FetchSealed identical ceremony to the Mekari Global OTP path — the existing OTP sheet (web + mobile) works unchanged
TERA single send → signing(otp) → download Seal(SealStamp)new kind stamp signDescription defaults to TIMESTAMP; routed via the new stampSealer split (STAMP_PROVIDER)
PERISAI tier/parallel (later) MultiSealer our envelope engine already models ordered/parallel multi-sign; map when staging proves the single flow
System Signature setKey → send → signing(session) adapter methods ready (SetSystemKey, SystemSend, SystemSign) unattended server-side signing — session = HMAC-SHA256(hashingKey, concat(orderIds)), verified against the spec's own worked example. Candidate for org-level auto-stamping later
Form User + e-KYC + cert check/renewal + specimen adapter methods (RegisterUser, SubmitEKYC, CertificateStatus, SetSpecimen*) powers the PSrE activation journey (mirrors ActivatePSrE); video eKYC capture is a UI project of its own — see rollout
TERA user register/status/deregister adapter methods an admin toggles which users may stamp
KEYLA (dynamic-token MFA) adapter methods (register QR / verify) optional replacement for SMS OTP; not in the first slice
document/status/by/type, cancel FetchSealed state probe + order cancel pending/expired/complete mapping for the poll sweeper

Job identity: pending seals store ProviderJobID = "perisai:<orderId>" or
"tera:<orderId>" so the poll sweeper and the OTP path route back to the right product
endpoints. OTP tokens (GetOTP returns token; the SMS carries otpCode) are cached
in-process per order; RequestOTP refreshes them. The spec marks GET /otp/... as
deprecated while the signing body still requires its token — first thing to verify on
staging
; the adapter keeps the call and will tell us.

Config

Env Meaning Default
ESIGN_PROVIDER=signit PERISAI becomes the Official signature tier existing enum grows
STAMP_PROVIDER who serves the new stamp seal kind (none\|mock\|signit) none (empty follows ESIGN_PROVIDER when that is signit)
SIGNIT_ENV which gateway to use (staging\|production) staging
SIGNIT_BASE_URL gateway origin; optional on staging, required on production staging preset, empty on production
SIGNIT_CLIENT_ID / SIGNIT_CLIENT_SECRET daily-token Basic credentials — (adapter reports unavailable when unset)
SIGNIT_CORPORATE_ID path segment on every call
SIGNIT_API_VERSION {VersiAPI} path segment v1

Validation is fail-closed: ESIGN_PROVIDER=signit (or STAMP_PROVIDER=signit) with
missing id/secret/corporateId refuses to boot, same pattern as Mekari/Peruri.

Going to production

A signature is the one thing here that fails quietly when it is pointed at the wrong
environment. A staging ceremony succeeds end to end — the OTP is accepted, the audit chain
records it, the viewer shows a signed document with a meterai — and the certificate behind
it is a test certificate with no legal standing. Nobody finds out until a document is
disputed, which is the moment the signature existed for.

So the two halves of the chain are provisioned separately (Peruri issues the e-Meterai
account; Sign-IT issues the signing gateway) and "one went live, the other did not" is the
expected mistake. The config layer refuses these combinations at boot:

Configuration Result
PERURI_ENV=production + SIGNIT_ENV=staging refuses to start — signatures would be minted in the sandbox
SIGNIT_ENV=production + PERURI_ENV=staging refuses to start — e-Meterai would be stamped in the sandbox
SIGNIT_ENV=production with no SIGNIT_BASE_URL refuses to start — the production origin comes with the contract and is not guessed by the binary
SIGNIT_ENV=production still holding the apistg. URL refuses to start
ESIGN_MOCK_OTP=true alongside any production provider refuses to start — a fixed OTP in front of a live signing key

The staging origin is the only host the binary names for itself. There is no built-in
production URL: an invented one would look authoritative and be wrong. Put the origin Peruri
gives you in SIGNIT_BASE_URL and set SIGNIT_ENV=production next to it.

Go-live checklist:

  1. PERURI_EMAIL / PERURI_PASSWORD switched to the production account, PERURI_ENV=production.
  2. SIGNIT_CLIENT_ID / SIGNIT_CLIENT_SECRET / SIGNIT_CORPORATE_ID switched to the
    production set, SIGNIT_ENV=production, SIGNIT_BASE_URL set to the contracted origin.
  3. ESIGN_MOCK_OTP unset or false (the guard enforces this).
  4. Top up the meterai/credit pool — see docs/PERURI_CREDITS.md; the metering is a local
    ledger and neither provider exposes a balance API, so a depleted quota surfaces as a
    failed ceremony rather than a warning.
  5. Sign one throwaway document and verify it in a third-party PDF reader (not only in
    Obscura) — the certificate chain is the thing being checked, and that is what a staging
    certificate fails.

Rollout (updated 2026-07-17)

  1. ~~Token smoke~~ DONE — blocked at step 2 by the corp binding (03).
  2. Once Peruri activates the binding: register a test user, walk e-KYC with
    Peruri's staging tools, certificate/check until issued, set a specimen; resolve
    the OTP-vs-KEYLA question live. Then TERA: register the affixer's email in
    Admin → Security → TERA users and run the stamp ceremony on staging
    (STAMP_PROVIDER=signit).
  3. ~~TERA e-stamp slice~~ BUILT (see above) — mock-proven; needs the staging
    live-proof only.
  4. ~~Activation journey~~ BUILT 2026-07-18: the /activation wizard (consent with
    Peruri's exact wording, OCR-assisted KTP form, phone OTP, LIVE browser camera
    liveness capture, specimen, certificate status) over a provider-neutral
    PSrEJourney port (Sign-IT + dev-mock implementations), with KYC gating on both
    the PSrE sign tier and the stamp ceremony. Mock-proven in a scripted browser
    walk. Remaining for production: a real SMS/WhatsApp gateway for the phone OTP,
    a vision-capable AI for the KTP OCR assist (degrades to manual), the renewal
    submission, and the staging live-proof once Peruri unblocks the corp binding.
  5. Later: tier/parallel via our envelope engine, System Signature for unattended
    org stamps, KEYLA as an OTP alternative.

Open questions for Peruri (ask alongside the creds)

  • GET /otp/{v}/{corp}/sign is "no longer implemented" but signing still requires its
    token — what is the current token source for non-KEYLA signing?
  • Is System Signature available for TERA (spec shows it for PERISAI single only)?
  • TERA quota: error 71 exists but no quota-inquiry endpoint — how do we check balance?
  • Prod base URL + rate limits + webhook/callback availability (spec documents none — we
    poll status/by/type).
  • e-KYC video constraints (duration/size/prompt script) for building the capture UI.