API/Sign-in
Email a one-time sign-in code
POST/v1/sign-ins· no key needed
Starts a sign-in by emailing a six-digit code to the address given.
Confirm it with POST /v1/sign-ins/{sign_in_id}/confirmation to get
a key. This creates the account if the address is new, and adds a key
to the account if it is not.
The response is identical whether or not an account exists. Anything else would let this endpoint answer "is this person a customer?" for any email address.
Every request spends a solved challenge from
POST /v1/sign-in-challenges, passed as altcha. After that:
- One code per address per minute. Asking again sooner is
429withRetry-After. - One open sign-in per address. A new code cancels the previous
one, which then answers
410. Holding several codes open would otherwise multiply the attempts. - A code expires ten minutes after it is sent, can be used once, and allows five attempts.
- Requests are also rate-limited per email address and per IP.
- A global cap on sign-in emails per hour answers
503when reached.
The email says plainly that we never ask for the code, and shows when and from where the request was made, so its owner can tell whether it was theirs. This is the only way into the API that requires no key.
curl -X POST https://api.einvoicing.dev/v1/sign-ins \ -H "Content-Type: application/json" \ -d '{"email":"you@company.com","altcha":"<altcha>"}'<?php$client = new GuzzleHttp\Client();$response = $client->request('POST', 'https://api.einvoicing.dev/v1/sign-ins', [ 'headers' => [ 'Accept' => 'application/json', ], 'json' => [ 'email' => 'you@company.com', 'altcha' => '<altcha>', ],]);$data = json_decode((string) $response->getBody(), true)['data'];payload := `{ "email": "you@company.com", "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": "you@company.com", "altcha": "<altcha>" }),});const { data } = await res.json();Request body
application/json· required
| Field | Type | Description |
|---|---|---|
emailrequired | string · email | Where to send the code. This is the account's identity; there is no username. |
altcharequired | string | A challenge from POST /v1/sign-in-challenges, solved: the
base64 of {"challenge": <data>, "solution": {"counter": <n>,
"derivedKey": "<hex>"}}. This is exactly what ALTCHA's solvers
produce.
|
Response
202 A code has been sent, if the address can receive one. Keep id:
it is the only thing the code will work with.
| Field | Type | Description |
|---|---|---|
datarequired | SignIn | A pending sign-in. Its code arrives by email. |
{ "data": { "id": "01J9Z3K4Q7VN3XW2R5T6Y8B0CD", "email": "steve@example.com", "expires_at": "2026-09-11T14:13:11.482Z" }}Errors
Every error is application/problem+json (RFC 9457). Branch on type, which is stable, never on title or detail.
| Status | When |
|---|---|
| 400 | The body could not be parsed as the XML or JSON its |
| 422 | Either the challenge was not accepted ( |
| 429 | A code was sent to this address in the last minute, or a
per-address or per-IP limit was reached. Wait for the number of
seconds in |
| 503 | The global cap on sign-in emails per hour has been reached, so
new sign-ins are paused until volume falls. It is not a fault in
the request, and existing keys are unaffected. Problem |
Schemas
SignInRequest
The address to send a one-time code to, and a solved challenge.
| Field | Type | Description |
|---|---|---|
emailrequired | string · email | Where to send the code. This is the account's identity; there is no username. |
altcharequired | string | A challenge from POST /v1/sign-in-challenges, solved: the
base64 of {"challenge": <data>, "solution": {"counter": <n>,
"derivedKey": "<hex>"}}. This is exactly what ALTCHA's solvers
produce.
|
SignIn
A pending sign-in. Its code arrives by email.
| Field | Type | Description |
|---|---|---|
idrequired | Ulid | A ULID. Opaque and time-ordered. |
emailrequired | string · email | The address the code was sent to, trimmed and lower-cased. This is the form the account is identified by. |
expires_atrequired | Timestamp · date-time | RFC 3339, UTC, millisecond precision. |