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/allignores 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 samesingle/signingendpoint withotpCodeempty and the
signer's 8-digit KEYLA-app token intoken(+orderId,signDescription).
KEYLA implementation is mandatory ("implementasi Keyla adalah wajib"); an Email-OTP
option can be requested from Peruri separately. Implemented asSIGNIT_MFA=keyla
(default) |otp; in KEYLA mode the deprecatedGET /otp/.../signis never called.
Live-verified: the demo ceremony now reaches53 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. Thesignerarray must live insidepayload(Postman V1.5). A top-level
signeris accepted at send but silently discarded → signing fails
47 No Signature Coordinate Found.
2.varLocationmust 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/signaturesets 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
\ror Basic auth fails with82. GET /auth/v1/token/generateworks: envelope00, daily token expiring 23:59:59 WIB.
Staging gateway self-reportsv24.11.28(prod reportsv26.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-CLIENTreturns
03 "User Tidak Diperkenankan"(not the spec's meaning of 03). Discriminator: an
unknown corporate returns02 "Sistem ID tidak terdaftar", soPDS-CLIENTis 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 indocs/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 viaMETERAI_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/generatewithBasic base64(clientId:clientSecret)→
beareraccessTokenvalid 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 ontokernel.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-leftlowerLeft/upperRight + pagerect 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:
PERURI_EMAIL/PERURI_PASSWORDswitched to the production account,PERURI_ENV=production.SIGNIT_CLIENT_ID/SIGNIT_CLIENT_SECRET/SIGNIT_CORPORATE_IDswitched to the
production set,SIGNIT_ENV=production,SIGNIT_BASE_URLset to the contracted origin.ESIGN_MOCK_OTPunset orfalse(the guard enforces this).- 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. - 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)
- ~~Token smoke~~ DONE — blocked at step 2 by the corp binding (03).
- Once Peruri activates the binding: register a test user, walk e-KYC with
Peruri's staging tools,certificate/checkuntil 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). - ~~TERA e-stamp slice~~ BUILT (see above) — mock-proven; needs the staging
live-proof only. - ~~Activation journey~~ BUILT 2026-07-18: the
/activationwizard (consent with
Peruri's exact wording, OCR-assisted KTP form, phone OTP, LIVE browser camera
liveness capture, specimen, certificate status) over a provider-neutral
PSrEJourneyport (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. - 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}/signis "no longer implemented" butsigningstill 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
pollstatus/by/type). - e-KYC video constraints (duration/size/prompt script) for building the capture UI.