Send documents securely and get indisputable confirmation of receipt and reading. The recipient authenticates with an electronic ID (eID) from Auðkenni before the document opens and becomes available to them. The web service integrates the deliveries directly into your own systems and interfaces. One REST API, one endpoint to get started, and a test environment you can try today.
The whole integration takes about 3 days: create a free trial account, email hallo@justikal.com to get API access, build and test in the test environment, then email again to finalize a contract and get access to the production environment. An experienced backend developer has usually sent the first document within half an hour of receiving the API key — many finish the entire integration in a single day. Skim the 7 integration steps below, then forward this page to your developer (if applicable).
The Registered eDelivery service is registered with and licensed by Byggðastofnun (the Icelandic Regional Development Institute) as a postal service. It can therefore be used, among other things, to send registered mail digitally in accordance with the relevant provisions of Icelandic law. Your system uploads a PDF document and tells Justikal who should receive it. Justikal notifies the recipient, who must authenticate with an electronic ID (eID) before the document opens. Every step — sent, opened, signed — is timestamped in an event log, and you can download a formal delivery receipt certificate for every successful delivery.
pdfFile, recipientName,
recipientPersonalCode, recipientEmail, message,
services…). Everything your code does, you can first try here by hand.The green steps are yours; the dark steps are handled by Justikal. Only three of them involve writing code.
Go to demo.justikal.com — Justikal's test environment — and sign up. You'll get a dashboard where you will later see every test delivery your code creates.
Email hallo@justikal.com and request API access. Include the email address you used when creating the trial account so the two can be linked.
You receive an API key (starting with jkt_) for the test environment at
demoapi-is.justikal.com. Treat it like a password: keep it
server-side only, ideally in a secret store — never in browser code, Git or
logs.
One API call sends a document (detailed walkthrough below). Send it to yourself, open the delivery link, authenticate, and watch the status change in the dashboard.
Pick your level: Minimal integration (send documents, track status by polling or in the dashboard), Full integration (webhooks, electronic signatures, automation) or Advanced integration (entirely under your own brand — your URL, your notifications, Justikal invisible). Start with the minimum — you can upgrade at any time.
Does the integration work in the test environment? Email hallo@justikal.com again to finalize a service agreement and confirm which additional services you need (SMS, electronic signatures, delivery receipt certificates).
Justikal reviews the integration and then issues a production environment API
key. Switch the base URL from demoapi-is.justikal.com to
api-is.justikal.com (the URLs are otherwise identical), run through the
checklist — and you're all set.
| Item | Value |
|---|---|
| Test environment base URL | https://demoapi-is.justikal.com |
| Production environment base URL | https://api-is.justikal.com |
| Endpoint base path | /external/api/v1/ |
| Authentication | Authorization: Bearer jkt_your_key in every call |
| Interactive documentation | /external/api/doc (Swagger UI) · /external/api/doc.json (OpenAPI spec) |
| Rate limits | 100 calls/minute per key (429 + Retry-After if exceeded) |
| Test PDF | Any PDF, up to 300 MB — a one-page test document is enough |
You make one HTTP call that uploads a PDF document and tells Justikal who should
receive it. Justikal then emails the recipient a secure link on your behalf. The call is a
plain multipart/form-data POST — the same format as a file upload form on any
website — so every HTTP client supports it out of the box.
# Replace the key and use your own email address as the recipient,
# so YOU receive the delivery and see the whole experience.
curl -X POST "https://demoapi-is.justikal.com/external/api/v1/edeliveries" \
-H "Authorization: Bearer jkt_your_key" \
-F "pdfFile=@/path/to/document.pdf" \
-F "recipientName=Jón Jónsson" \
-F "recipientCountry=IS" \
-F "recipientPersonalCode=1234567890" \
-F "recipientEmail=you@yourcompany.com" \
-F "emailSubject=A test delivery from us" \
-F "message=Please review this document and confirm receipt."
import fs from "node:fs";
const form = new FormData();
form.append("pdfFile", new Blob([fs.readFileSync("document.pdf")]), "document.pdf");
form.append("recipientName", "Jón Jónsson");
form.append("recipientCountry", "IS");
form.append("recipientPersonalCode", "1234567890");
form.append("recipientEmail", "you@yourcompany.com");
form.append("emailSubject", "A test delivery from us");
form.append("message", "Please review this document and confirm receipt.");
const res = await fetch(
"https://demoapi-is.justikal.com/external/api/v1/edeliveries",
{ method: "POST",
headers: { Authorization: `Bearer ${process.env.JUSTIKAL_TOKEN}` },
body: form });
const delivery = await res.json();
import os, requests
with open("document.pdf", "rb") as f:
res = requests.post(
"https://demoapi-is.justikal.com/external/api/v1/edeliveries",
headers={"Authorization": f"Bearer {os.environ['JUSTIKAL_TOKEN']}"},
files={"pdfFile": f},
data={
"recipientName": "Jón Jónsson",
"recipientCountry": "IS",
"recipientPersonalCode": "1234567890",
"recipientEmail": "you@yourcompany.com",
"emailSubject": "A test delivery from us",
"message": "Please review this document and confirm receipt.",
})
delivery = res.json()
{
"id": "550e8400-e29b-41d4-a716-446655440000", ← store this — everything else refers to it
"status": "pending",
"delivery_url": "https://delivery.justikal.is/550e8400-...",
"created_at": "2026-08-11T13:54:31+00:00"
}
Done — the recipient has an email. Store the id; every other endpoint
(status, event log, PDF download, report, revocation) refers to it.
400 with
“At least one of recipient_email or recipient_phone must be provided” — you left out
a recipient field, or set recipientEmail without emailSubject.
The message in the error response always says exactly which field to fix.Worth understanding before you integrate — this is the experience your customers get, and exactly what you'll see when you send the first delivery to yourself.
A delivery has one status at a time; timestamps and the
event log hold the details. This is what your integration tracks:
status becomes delivered, the delivery receipt certificate is ready for download.All three levels are fully valid in the production environment. The minimal level gets you live fastest, full integration automates everything, and advanced integration makes the experience entirely yours — your domain, your brand, your notifications.
POST /edeliveries — the call you saw above.GET /edeliveries?status=pending a few
times a day, or simply review the deliveries in the
dashboard — no status polling in code
required.delivered, download
GET /edeliveries/{id}/deliveryReport.postbackUrl at creation — Justikal POSTs to you
on every status change. No polling, instant reactions.disableNotifications: true and
send the delivery_url yourself from your own system.companyBrandingUsed,
senderNameHidden, lang (EN/IS).services.<iframe> on your own domain —
e.g. docs.yourcompany.com/doc=73883.notifications@yourcompany.com) — Justikal sends nothing.Exactly the first call you made earlier. In production code the only change is reading the key from configuration and handling the response.
Option 1 — no code: sign in to the dashboard and see every delivery, its status and event log. Perfectly adequate for low volume.
Option 2 — look up a single delivery:
curl "https://demoapi-is.justikal.com/external/api/v1/edeliveries/{id}" \
-H "Authorization: Bearer jkt_your_key"
# → status, recipient and timestamps: created_at, delivered_at, signed_at, deadline_at
Option 3 — sweep everything pending (recommended for polling — one call instead of one per delivery, friendly to the 100 calls/min rate limit):
curl "https://demoapi-is.justikal.com/external/api/v1/edeliveries?status=pending&limit=50" \
-H "Authorization: Bearer jkt_your_key"
Run this on a schedule — every 15–30 minutes is more than enough for most document workflows.
# The formal delivery receipt certificate (returns 404 until delivery is complete)
curl ".../edeliveries/{id}/deliveryReport" -H "Authorization: Bearer jkt_..." -o receipt.pdf
# The document itself — use type=signed once signed_at is set
curl ".../edeliveries/{id}/pdf?type=signed" -H "Authorization: Bearer jkt_..." -o signed.pdf
Done. This is a fully valid minimal integration: one send call, one scheduled status sweep, one download when the job is finished.
What you're doing: you give Justikal an HTTPS URL on your web server that gets POSTed to every time a delivery's status changes — your system reacts within seconds instead of waiting for the next poll.
// Express example — the pattern matters more than the framework
app.post("/webhooks/justikal", async (req, res) => {
res.sendStatus(200); // 1. respond immediately
const { id } = req.body;
const delivery = await getDelivery(id); // 2. re-fetch: the API is the source of truth
await handleStatusChange(delivery); // 3. idempotent: the same event twice = harmless
});
curl -X POST "https://demoapi-is.justikal.com/external/api/v1/edeliveries" \
-H "Authorization: Bearer jkt_your_key" \
-F "pdfFile=@contract.pdf" \
-F "recipientName=Jón Jónsson" \
-F "recipientCountry=IS" \
-F "recipientPersonalCode=1234567890" \
-F "recipientEmail=jon@example.com" \
-F "emailSubject=Your contract is ready for signature" \
-F "postbackUrl=https://yourapp.com/webhooks/justikal" \
-F "expiresAt=2026-09-11" \
-F "lang=IS" \
-F "services[]=e_delivery_signature"
| Field | What it unlocks |
|---|---|
disableNotifications: true | Justikal sends nothing — you send
the delivery_url (or short_url) from your own branded system.
Webhooks keep working. |
companyBrandingUsed: true | Your logo on the delivery page. |
senderNameHidden: true | Only the company name is shown, not an individual's. |
lang | Notification language (e.g. EN, IS). |
services | e_delivery_signature (the recipient signs
electronically), e_delivery_sms (SMS channel — requires recipientPhone +
smsText), e_delivery_seal (electronically sealed delivery receipt
certificate). Confirm with Justikal what your contract includes. |
<iframe>
on your own domain. That's the advanced integration below.# Revoke a delivery that hasn't been opened (only while status = pending)
curl -X POST ".../edeliveries/{id}/revoke" -H "Authorization: Bearer jkt_..."
# The full event log, event by event
curl ".../edeliveries/{id}/audit" -H "Authorization: Bearer jkt_..."
# GDPR: delete + anonymize personal data (permanent — use revoke for ordinary revocation)
curl -X DELETE ".../edeliveries/{id}" -H "Authorization: Bearer jkt_..."
For companies that want nothing visible from Justikal in the customer journey: emails come from your address, the URL is yours, and the delivery flow happens on your own URL. Justikal runs “headless” behind the scenes.
What you're doing: the same creation call as always, but with the flags that shift the customer experience over to you. This is exactly the combination used in bank/collection integrations running in production:
curl -X POST "https://demoapi-is.justikal.com/external/api/v1/edeliveries" \
-H "Authorization: Bearer jkt_your_key" \
-F "pdfFile=@Letter_884.pdf" \
-F "recipientName=Jón Jónsson" \
-F "recipientCountry=IS" \
-F "recipientPersonalCode=1234567890" \
-F "recipientEmail=jon@example.com" \
-F "emailSubject=placeholder" \
-F "expiresAt=2027-01-10" \
-F "disableNotifications=true" \ # ★ Justikal sends no email or SMS
-F "companyBrandingUsed=true" \ # your brand on the delivery view
-F "senderNameHidden=true" \ # company name only, no individual
-F "hasAdditionalSecurity=true" \
-F "postbackUrl=https://ws.yourcompany.com/deliv-endpoint" \
-F "lang=IS"
The response contains delivery_url — the starred fields
(disableNotifications and postbackUrl) are the keys the whole
pattern rests on.
What you're doing: you store the delivery_url against your own
document number and display it in an iframe on a page you control. The recipient's browser
never shows a Justikal URL:
<!-- https://docs.yourcompany.com/doc=73883 — your page, your brand -->
<iframe
src="https://delivery.justikal.com/is/e9842df4-1b9a-4d43-bccc-a06d00245d6b?lang=is"
style="width:100%; height:100vh; border:0"
allow="publickey-credentials-get">
</iframe>
Append ?lang=is (or en) to the URL to control the language of
the embedded view. eID authentication, document display and delivery confirmation
all happen inside the frame.
What you're doing: since Justikal stays silent, you email or text the recipient
from your own system — your sender address, your template, your deadline in the copy — with
a link to your page from step C2 (not to the raw delivery_url).
The postbackUrl webhook (same pattern as in path B) updates
your own status displays and drives your next steps — reminders from your address,
case updates in your core system, automatic download of the confirmation on delivery.
| Touchpoint | What the customer sees |
|---|---|
| Email / SMS notification | You — your address, your template |
| Link & browser URL | You — docs.yourcompany.com |
| The document page | You — your brand, with the delivery view embedded (company branding inside the frame, sender name hidden) |
| eID authentication | Auðkenni's standard flow inside the frame — required by law and familiar to users |
| Delivery receipt certificate (legal proof) | A Justikal-sealed PDF |
GET /edeliveries/{id}/deliveryReport once the status is
delivered: the document name and its SHA-256 hash, the recipient's name
and personal ID number, the exact delivery timestamp (UTC), the authentication method and
certificate issuer (Auðkenni), the sender's identity, the send time and deadline, and the
delivery's unique number — all under a fully qualified electronic seal that makes the
certificate tamper-proof.All errors share the same JSON format — an error code plus a readable
message that names the field or rule in question:
| Status | Meaning | Fix |
|---|---|---|
400 | A field is missing/invalid or a business rule was violated | Read the message, fix the call, try again |
401 | The key is missing or invalid | Check the Authorization: Bearer jkt_... header |
404 | Unknown ID — or the report isn't ready yet | Verify the ID; for reports, wait for delivered |
429 | Over 100 calls/minute | Wait Retry-After seconds |
500 | An error on Justikal's side | Retry with exponential backoff; contact support if it persists |
api-is.justikal.com500 retried with exponential backoff;
Retry-After respected on 429.hasAdditionalSecurity decided per use case (default
true = identity checked against the personal ID number).services) in your contract confirmed with Justikal.