CodeB eForms Server by Aloaha

API REST API: forms and e-invoices from your own software.

Every form on the server can be used from other applications: list forms, read their fields, check data, get a filled PDF, submit, or create a ZUGFeRD / XRechnung e-invoice from structured data. The API is described as OpenAPI 3.1 and protected with bearer tokens of your OpenID Connect servers.

A submission through the API is processed exactly like one from the web form: sealed PDF, optional server signature, e-mails, e-invoice.

At a glance

  • Base URL: https://<your server>/api/v1 (on servers without extensionless URLs: /api.ashx/v1).
  • Description: OpenAPI 3.1 at /api/v1/openapi on every server, and as a download. Import it into Postman, Swagger UI or a code generator.
  • Format: JSON in and out (UTF-8). PDFs come as Base64 in the JSON answer, or as the file itself with Accept: application/pdf.
  • Errors: RFC 9457 problem details (application/problem+json).

Authentication

Every call except /, /health, /openapi and /field-types needs an access token of one of the OpenID Connect servers configured on the form server (by default phone.codeb.io, www.aloaha.com and phone.aloaha.com):

Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6…
  • JWT tokens are checked locally: RS256 signature against the server's published keys, issuer, expiry and audience. The audience is the form server's client ID; an administrator can allow further client IDs (settings page, REST API).
  • Opaque tokens are checked by token introspection at the issuing server. If several servers are configured, name it in the header X-OIDC-Issuer: phone.codeb.io.
  • Roles come from the token's role claim, like the sign-in to the designer: admin and user see the forms of their group (the sign-in server's host name), guest only released forms. Submitting needs user or admin, unless the administrator allows guests.
  • GET /api/v1/me shows which user, role and group a token stands for.

Getting a bearer token

The API accepts the access tokens the OpenID Connect servers issue at sign-in (RS256 JWTs, valid for one hour). Your application gets one with the standard authorization code flow with PKCE; any OAuth 2.0 / OpenID Connect library can do this. Below, phone.codeb.io stands for the sign-in server you use.

1. Register your application once

  • At the sign-in server, register your application as an OIDC client (an administrator of the CodeB server does this on /oidc-clients.html, or ask us at info@aloaha.com). You choose a client ID (e.g. my-app) and the redirect URI the sign-in returns to (e.g. http://localhost:8765/callback for a script). The redirect URI must match byte for byte. With a client secret the client is confidential, without one it is a public client and uses PKCE only.
  • On the form server, an administrator allows the client ID: Settings → REST API → Other applications allowed. Tokens for the form server's own client ID are accepted anyway.

2. Sign in and get the code

Create a PKCE pair and open the authorization URL in a browser. The user signs in (password, passkey or EU Digital Identity Wallet), and the browser returns to your redirect URI with ?code=….

VERIFIER=$(openssl rand -base64 48 | tr -d '=+/' | cut -c1-64)
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')

# open this URL in the browser (one line)
https://phone.codeb.io/oauth2/v1/authorize?response_type=code&client_id=my-app
  &redirect_uri=http%3A%2F%2Flocalhost%3A8765%2Fcallback&scope=openid%20profile%20email
  &state=xyz&nonce=abc&code_challenge=$CHALLENGE&code_challenge_method=S256

Even if nothing listens on the redirect URI, the code is in the browser's address bar. It is valid for a few minutes and only once.

3. Exchange the code for tokens

curl -X POST https://phone.codeb.io/oauth2/v1/token \
  -d grant_type=authorization_code -d client_id=my-app \
  -d redirect_uri=http://localhost:8765/callback \
  -d code=$CODE -d code_verifier=$VERIFIER
# confidential client: add  -d client_secret=$SECRET

{ "access_token": "eyJhbGciOiJSUzI1NiIs…", "token_type": "Bearer", "expires_in": 3600,
  "id_token": "eyJ…", "refresh_token": "…" }

4. Call the API, refresh when needed

curl -H "Authorization: Bearer $ACCESS_TOKEN" https://forms.example.com/api/v1/me

# after one hour: new tokens with the refresh token (it rotates on every use)
curl -X POST https://phone.codeb.io/oauth2/v1/token \
  -d grant_type=refresh_token -d client_id=my-app -d refresh_token=$REFRESH_TOKEN

The token acts for the user who signed in: their role decides which forms the API shows (see above). For a background job, sign in once with a dedicated account and keep the token fresh with the refresh token.

The whole flow as a script

Python 3, standard library only: opens the browser, receives the code on localhost and prints the access token.

import base64, hashlib, http.server, json, secrets, urllib.parse, urllib.request, webbrowser

ISSUER, CLIENT_ID = "https://phone.codeb.io", "my-app"
REDIRECT = "http://localhost:8765/callback"
verifier = secrets.token_urlsafe(48)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
state = secrets.token_urlsafe(16)
webbrowser.open(ISSUER + "/oauth2/v1/authorize?" + urllib.parse.urlencode({
    "response_type": "code", "client_id": CLIENT_ID, "redirect_uri": REDIRECT,
    "scope": "openid profile email", "state": state, "nonce": secrets.token_urlsafe(16),
    "code_challenge": challenge, "code_challenge_method": "S256"}))

class Callback(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        q = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
        assert q.get("state") == [state], "state mismatch"
        self.server.code = q["code"][0]
        self.send_response(200); self.end_headers(); self.wfile.write(b"Signed in. You can close this tab.")
    def log_message(self, *a): pass

srv = http.server.HTTPServer(("localhost", 8765), Callback)
srv.handle_request()
body = urllib.parse.urlencode({"grant_type": "authorization_code", "client_id": CLIENT_ID,
    "redirect_uri": REDIRECT, "code": srv.code, "code_verifier": verifier}).encode()
tokens = json.load(urllib.request.urlopen(ISSUER + "/oauth2/v1/token", body))
print(tokens["access_token"])

Endpoints

All paths are relative to /api/v1.

MethodPathWhat it does
GET/formsForms the token may use; filter with ?released=true and ?q=.
GET/forms/{formId}Settings, page count, links to the PDF and HTML versions.
GET/forms/{formId}/fieldsEvery field: type, name to submit, position, required, rule, options.
GET/forms/{formId}/schemaJSON Schema of the values, for code generators and AI agents.
GET/forms/{formId}/pdfThe empty released PDF form.
POST/forms/{formId}/validateChecks values without submitting.
POST/forms/{formId}/fillFilled PDF without submission, no e-mails (like Save in the web form).
POST/forms/{formId}/submissionsSubmits the form: sealed PDF, e-mails, e-invoice.
POST/einvoicesZUGFeRD / Factur-X / XRechnung invoice from structured data.
GET/field-types, /validation-rules, /meField types, validation rules, identity of the token.

Reading forms and fields

List the released forms, then read the fields of one form. Each input field has a submitName: that is the key to use when you submit. Radio buttons share the name of their group; drop-downs list their options.

curl -H "Authorization: Bearer $TOKEN" https://forms.example.com/api/v1/forms?released=true
curl -H "Authorization: Bearer $TOKEN" https://forms.example.com/api/v1/forms/zugferd/fields?inputOnly=true

Field types: textfield, dropdown, checkbox and radio take values; text, image, line and button are layout only. Positions are in points, measured like in the editor.

Filling and submitting

Send the values as an object of submitName and value. Check boxes take true or false, drop-downs and radio groups one of their options. The server checks required fields, options, length and validation rules first; if something is wrong it answers 422 with a list of errors and submits nothing.

curl -X POST https://forms.example.com/api/v1/forms/contactrequest/submissions \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"values": {"Name": "Erika Muster", "Email": "erika@example.com", "Message": "Please call me back.", "Consent": true}}'

Answer (201):

{
  "submissionId": "0b6c1f2e9d4a4c51a3f0d2b7e8c91a44",
  "formId": "contactrequest",
  "status": "submitted",
  "pdf": { "fileName": "contactrequest.pdf", "size": 84211, "sha256": "…", "eInvoice": false, "data": "JVBERi0xLjc…" }
}
  • /fill returns the filled PDF only: nothing is sealed, no e-mail is sent, required fields may stay empty.
  • Forms that require sign-in receive the verified identity of the token, exactly like after a sign-in in the browser. Other forms get it with "useIdentity": true.
  • Unknown field names are refused, so typing errors do not get lost. Send "ignoreUnknownFields": true to skip them instead. Fields you leave out keep their preset value, as in the browser.
  • Submitting needs the role user or admin; the server also limits the number of API submissions per day.

E-invoices (ZUGFeRD / XRechnung)

POST /einvoices takes an invoice as structured data, fills the server's invoice form and submits it. The answer is a PDF/A-3 with the EN 16931 XML embedded (ZUGFeRD / Factur-X; with a buyer reference (Leitweg-ID) for XRechnung). Line totals, VAT and the grand total are calculated when you leave them out. Dates are ISO (2026-10-11), amounts use a dot.

curl -X POST https://forms.example.com/api/v1/einvoices \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Accept: application/pdf" \
  -o invoice-2026-0042.pdf -d '{
  "invoice": { "number": "2026-0042", "date": "2026-10-11", "currency": "EUR", "dueDate": "2026-10-25", "taxPercent": 19 },
  "seller":  { "name": "Example Supplies GmbH", "street": "Musterstrasse 1", "postCode": "10115", "town": "Berlin",
               "country": "DE", "email": "billing@example.com", "vatId": "DE123456789" },
  "buyer":   { "name": "Example Hotel AG", "street": "Seestrasse 1", "postCode": "12345", "town": "Musterstadt",
               "country": "DE", "email": "accounts@example.com" },
  "payment": { "iban": "DE02120300000000202051", "bic": "BYLADEM1001" },
  "items":   [ { "name": "Consulting (hours)", "quantity": 4, "netPrice": 120 },
               { "name": "Travel flat rate", "netPrice": 80 } ]
}'

"action": "preview" returns the filled invoice without sending it. The invoice is e-mailed to the addresses in the data, like the invoice web form does.

Full reference of the e-invoice API: every field with its EN 16931 business term, calculation and VAT rules, XRechnung checklist and errors.

Errors and limits

Every error is a problem details object with type, title, status and detail; validation errors add a list of fields.

StatusCodeMeaning
400invalid_jsonThe body is not a JSON object.
401invalid_token, invalid_requestToken missing, expired, for another application, or from an unknown server.
403insufficient_scope, local_accountRequired scope missing, or the name belongs to a local account.
404form_not_foundUnknown form, or a form of another group.
409form_not_releasedThe form has not been released yet.
422validation_failedValues missing or invalid; see errors.
403role_requiredSubmitting needs the role user or admin.
404not_foundUnknown API path.
405method_not_allowedWrong HTTP method (GET or POST) for this path.
413 / 415too_large, unsupported_media_typeBody larger than 10 MB, or not JSON.
422not_an_invoice_form, too_many_itemsThe chosen form has no invoice lines, or fewer lines than the invoice.
429rate_limited, daily_limitToo many requests (default 120 per minute and user), or the daily cap of API submissions; wait for Retry-After.
502form_processing_failedThe form could not be processed (e.g. e-mail server not reachable).
500server_errorError on the server; details are in the server log.
503api_disabled, issuer_unavailable, loopback_not_configuredAPI switched off, sign-in server not reachable, or the internal address of the server is not set.

What the API covers

We compared the API with the APIs of common form builders and PDF services. They typically offer: listing forms and their questions, reading and creating submissions, webhooks on new submissions, and filling or reading PDF form data. The CodeB eForms Server API covers:

  • Forms and fields, including field types, positions, validation rules and a JSON Schema per form.
  • Submissions with the full processing of the web form: sealed and optionally signed PDF, e-mails and attachment rules.
  • PDF filling without submission, and the empty PDF form.
  • E-invoices (ZUGFeRD / Factur-X / XRechnung) from structured data, a feature form builders usually do not have.
  • Standards-based security: OAuth 2.0 bearer tokens of your own OpenID Connect servers, no extra API keys.

The server deliberately keeps no copy of submitted data, so there is no endpoint to list past submissions. Notifications of new submissions (webhooks) are planned.

Questions and answers

How do I get a token for the API?

Sign in with the authorization code flow (with PKCE) at one of the OpenID Connect servers configured on the form server, for example phone.codeb.io, and use the access token. Tokens issued for the form server's client ID are accepted; an administrator can allow the client IDs of further applications.

Is there an OpenAPI or Swagger description?

Yes. Every server publishes OpenAPI 3.1 at /api/v1/openapi, and you can download it from this page. Import it into Postman, Swagger UI or a code generator.

Can I create ZUGFeRD or XRechnung invoices through the API?

Yes. POST /api/v1/einvoices takes seller, buyer, lines and VAT as JSON and returns a PDF/A-3 with the EN 16931 XML embedded. With a buyer reference (Leitweg-ID) it meets XRechnung.

Does a submission through the API differ from one in the browser?

No. The API hands the values to the same processing as the web form, so the sealed PDF, the signature, the e-mails and the e-invoice are the same.

Can I list submitted forms?

No. The server does not keep submitted data; the result goes to the configured recipients and back to the caller. Notifications for new submissions are planned.

Try the API

Ask for test credentials for our online form server and try the API with your own forms.

Last updated: