An end-to-end guide for sending disclosed payroll metadata alongside sealed payslip bytes. Follow the sender, issuer, recipient, and verifier boundaries below, then try the live /payroll landing page or inspect the live envelope contract.
The payroll API is an envelope issuer and audit surface, not a payslip reader. The sender keeps the plaintext and decryption key in its own custody while the API receives the minimum metadata needed to identify, sign, hash, and persist the envelope.
The employer or payroll integrator encrypts the payslip locally, retains the plaintext and key custody, and prepares only jurisdiction, gross, deductions, and opaque encrypted_payload for submission.
POST /api/payroll/envelope receives those four fields. It never decrypts encrypted_payload; the sealed bytes remain opaque to this endpoint.
The server hashes the ciphertext, canonicalizes the envelope metadata, signs the canonical bytes with its Ed25519 issuer key, and persists the signed envelope for audit.
A recipient or downstream verifier checks the signed claim and disclosed metadata. If entitled to read the payslip, it follows an independently authorized decryption and key-delivery path outside this endpoint.
/api/payroll/envelopeJSON request with disclosed metadata and opaque encrypted_payload for the payslip bytes. One envelope per issuer signature; replay of the same inputs re-issues the same envelope deterministically.
The issuer signs a canonical UTF-8 string built from a structured object whose keys are sorted
alphabetically and rendered with no whitespace. The signed bytes cover
envelope_id, jurisdiction, gross, deductions,
ciphertext_hash, the issuer name, and issued_at. Two
different jurisdictions therefore always produce two distinct signed envelopes — the
signed canonical payload is the cross-border audit point, never the plaintext payslip.
The jurisdiction field must appear in the server-side whitelist. Unknown values return 400. The whitelist below is rendered straight from the live contract.
JSON.stringify(obj) with keys sorted alphabetically and no whitespace.
encrypted_payload stays sealed. It contains the encrypted payslip bytes and is never decrypted by the payroll API. The signed and disclosed metadata a verifier can see includes gross, deductions, jurisdiction, issuer, timestamp, envelope ID, and the ciphertext hash. Those fields support verification without exposing salary plaintext.
The signing key is server-owned. When PAYROLL_ISSUER_PRIVATE_KEY is unset the
endpoint still works but issues envelopes with an ephemeral key (mock_key: true);
those envelopes will not verify across restarts. The fingerprint below is the 16-character SPKI
body prefix a visitor can compare against the issuer… pill on
/payroll to confirm both pages describe the same signing identity.
mock_key — PAYROLL_ISSUER_PRIVATE_KEY is not set on the server. The full PEM below was generated at boot and is unstable across restart; envelope signatures will not verify once the process is replaced.
-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAoMrDBSJicqCEyHma4ZgvEBRK7agzAEXz2yqFdoYAlss= -----END PUBLIC KEY-----
The adjacent KAIOS receipt contract provides a reusable signed-receipt boundary. A receipt binds its
canonical payload and disclosed fields; POST /api/kaios/verify
checks the Ed25519 signature over the exact canonical payload_string and returns a verifier-safe
verdict containing the signed issuer, timestamp, and disclosed fields.
All four fields are required. Any missing or invalid field produces a 400 with the offending fields listed in errors.
| Field | Type | Description |
|---|---|---|
| jurisdiction | string | ISO-3166 alpha-2/3 jurisdiction code. Whitelisted server-side; unknown values return 400. |
| gross | number | Gross salary amount in the envelope's native currency. NUMERIC(18,2) on the server to avoid float drift on money. |
| deductions | object | Per-jurisdiction deductions keyed by code (e.g. US: { fica, federal }, UK: { ni, paye }, SG: { cpf }). Each value is a number. JSONB so per-jurisdiction shapes fit one schema. |
| encrypted_payload | string | Opaque encrypted payslip bytes. The server treats this as opaque and never decrypts; it is hashed and persisted verbatim for audit. |
The values below come straight from the live contract. This synthetic example contains disclosed gross and deductions metadata plus an opaque ciphertext placeholder; it is not a plaintext payslip or a real employee record.
{
"jurisdiction": "US",
"gross": 7500,
"deductions": {
"fica": 575,
"federal": 1200
},
"encrypted_payload": "0x9f3a8c1d_demo_us_payslip"
}
gross, deductions, and jurisdiction are intentionally disclosed API metadata. Only encrypted_payload represents the sealed payslip bytes, and the placeholder is synthetic.
One command — sends the example envelope to the live issuer. The host is whatever the request landed on; the path is the same everywhere.
curl -X POST https://automa-labs-ai.polsia.app/api/payroll/envelope \
-H "Content-Type: application/json" \
-d '{
"jurisdiction": "US",
"gross": 7500,
"deductions": {
"fica": 575,
"federal": 1200
},
"encrypted_payload": "0x9f3a8c1d_demo_us_payslip"
}'
jurisdiction with any value from the whitelist pills above; gross and deductions follow the per-jurisdiction shape (US fica / federal, UK ni / paye, SG cpf, …); encrypted_payload is the opaque ciphertext bytes the browser produced — the server treats it as opaque and never decrypts it.
Every terminal status the integrator needs to handle.
| Status | Body | When |
|---|---|---|
| 201 | {"ok":"true","envelope_id":"string (env_<sha256[:32]>)","jurisdiction":"string","gross":"number","deductions":"object","encrypted_payload_hash":"string (64-hex sha256 over the opaque encrypted_payload)","signature":"string (128-hex Ed25519)","payload_string":"string (canonical UTF-8 bytes the issuer signed)","issuer":"string","issued_at":"ISO-8601 string","issuer_pub_key_pem":"string (PEM SPKI Ed25519)","mock_key":"boolean","replay":"boolean — true if this is a replay of a previously-issued envelope (no DB write)"} |
Envelope accepted: signed canonical payload persisted to payroll_envelopes; envelope_id and signature returned. |
| 400 | {"ok":"false","errors":["string"]} |
Validation error — one or more required fields are missing, wrong type, or jurisdiction is not in the whitelist. |
A US-founded operating company can pay a contractor in SG, a UK entity can pay a contractor in DE, and a JP
employer can pay a contractor in BR — without shipping plaintext financial data across borders. Each envelope
is jurisdiction-bound: a US envelope carries the fica / federal deduction shape,
a UK envelope carries ni / paye, an SG envelope carries cpf, a DE envelope
carries soli / kirchensteuer, a JP envelope carries shotoku, and a BR
envelope carries inss. The JSONB deductions field fits each per-jurisdiction shape
without schema churn, the envelope_id is part of the signed canonical payload so a regulator can
confirm the issuance event without ever holding the payslip, and the encrypted_payload rides
along inside the same JSON document as opaque ciphertext — hash it, persist it, audit it. Two distinct
jurisdictions therefore always produce two distinct signed envelopes, and the issuance event is the audit
point — never the plaintext payslip bytes.
Every accepted envelope is written to the payroll_envelopes table, keyed UNIQUE on
envelope_id, and indexed on jurisdiction. The same contract — including
the example envelope and the response shapes — is published as plain JSON at
https://automa-labs-ai.polsia.app/api/payroll/envelope, so an integrator
can pull the spec straight off the live endpoint rather than caching this page. Return to the
/payroll landing page to see the live issuer surface.