Docs · Start
Getting started
Three requests get you a key, and a fourth validates a document. There is no dashboard and no password: the whole flow is the API.
1. Ask for a challenge
Every sign-in spends a solved proof-of-work challenge. It is the headless stand-in for a CAPTCHA: under a second with any ALTCHA solver, expensive in bulk.
curl -X POST https://api.einvoicing.dev/v1/sign-in-challenges<?php$client = new GuzzleHttp\Client();$response = $client->request('POST', 'https://api.einvoicing.dev/v1/sign-in-challenges', []);$data = json_decode((string) $response->getBody(), true)['data'];req, _ := http.NewRequest(http.MethodPost, "https://api.einvoicing.dev/v1/sign-in-challenges", nil)res, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer res.Body.Close()const res = await fetch("https://api.einvoicing.dev/v1/sign-in-challenges", { method: "POST",});const { data } = await res.json();2. Ask for a code
Solve the challenge, send it as altcha with your email address, and a six-digit code is emailed to you. This creates the account if the address is new. The response is the same either way, so the endpoint cannot be used to ask whether an address is a customer.
curl -X POST https://api.einvoicing.dev/v1/sign-ins \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]","altcha":"<altcha>"}'<?php$client = new GuzzleHttp\Client();$response = $client->request('POST', 'https://api.einvoicing.dev/v1/sign-ins', [ 'headers' => [ 'Accept' => 'application/json', ], 'json' => [ 'email' => '[email protected]', 'altcha' => '<altcha>', ],]);$data = json_decode((string) $response->getBody(), true)['data'];payload := `{ "email": "[email protected]", "altcha": "<altcha>"}`req, _ := http.NewRequest(http.MethodPost, "https://api.einvoicing.dev/v1/sign-ins", strings.NewReader(payload))req.Header.Set("Content-Type", "application/json")res, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer res.Body.Close()const res = await fetch("https://api.einvoicing.dev/v1/sign-ins", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ "email": "[email protected]", "altcha": "<altcha>" }),});const { data } = await res.json();3. Exchange the code for a key
Confirm with the code and the key comes back. The secret appears in that response and nowhere else, ever — it is stored only as an HMAC, so nobody can hand it back to you. Put it somewhere safe before you make the next request.
curl -X POST https://api.einvoicing.dev/v1/sign-ins/<sign_in_id>/confirmation \ -H "Content-Type: application/json" \ -d '{ "code": "482913", "key": { "name": "CLI on steve-laptop", "mode": "live" } }'<?php$client = new GuzzleHttp\Client();$response = $client->request('POST', 'https://api.einvoicing.dev/v1/sign-ins/<sign_in_id>/confirmation', [ 'headers' => [ 'Accept' => 'application/json', ], 'json' => [ 'code' => '482913', 'key' => [ 'name' => 'CLI on steve-laptop', 'mode' => 'live', ], ],]);$data = json_decode((string) $response->getBody(), true)['data'];payload := `{ "code": "482913", "key": { "name": "CLI on steve-laptop", "mode": "live" }}`req, _ := http.NewRequest(http.MethodPost, "https://api.einvoicing.dev/v1/sign-ins/<sign_in_id>/confirmation", strings.NewReader(payload))req.Header.Set("Content-Type", "application/json")res, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer res.Body.Close()const res = await fetch("https://api.einvoicing.dev/v1/sign-ins/<sign_in_id>/confirmation", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ "code": "482913", "key": { "name": "CLI on steve-laptop", "mode": "live" } }),});const { data } = await res.json();4. Validate something
Send the document you generate today. A document that breaks the rules is still a successful request: 200with valid: false. Nothing you send is stored.
curl -X POST https://api.einvoicing.dev/v1/validations \ -H "Authorization: Bearer $EINVOICING_API_KEY" \ -H "Content-Type: application/xml" \ --data-binary @invoice.xml<?php$client = new GuzzleHttp\Client();$response = $client->request('POST', 'https://api.einvoicing.dev/v1/validations', [ 'headers' => [ 'Authorization' => 'Bearer ' . getenv('EINVOICING_API_KEY'), 'Accept' => 'application/json', ], 'json' => [ 'document' => '<document>', ],]);$data = json_decode((string) $response->getBody(), true)['data'];payload := `{ "document": "<document>"}`req, _ := http.NewRequest(http.MethodPost, "https://api.einvoicing.dev/v1/validations", strings.NewReader(payload))req.Header.Set("Authorization", "Bearer "+os.Getenv("EINVOICING_API_KEY"))req.Header.Set("Content-Type", "application/json")res, err := http.DefaultClient.Do(req)if err != nil { log.Fatal(err)}defer res.Body.Close()const res = await fetch("https://api.einvoicing.dev/v1/validations", { method: "POST", headers: { Authorization: `Bearer ${process.env.EINVOICING_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "document": "<document>" }),});const { data } = await res.json();Keys
A key is presented as Authorization: Bearer <key>, or as X-API-Keyif that suits your client better. Keys look like einv_live_… or einv_test_…, and end in a checksum so a leaked one can be spotted by a secret scanner without asking us.
- Test keys call every product operation with real results and are never metered, so CI costs nothing. They cannot manage keys or billing.
- An account can hold several keys. That is how a key is rotated without downtime: create the new one, deploy it, then revoke the old one.
- Lost every key? Sign in again. The same flow adds a key to the same account.
Base URL
https://api.einvoicing.devParameters, the report's shape, and every error it can answer with.
The three layers, and what a finding actually tells you.