Device-flow sign-in
A client that cannot hold a browser session (a terminal, a CI runner, a device) asks for a short code; a person approves it in the dashboard; the client receives a freshly minted API key. The shape follows RFC 8628. No plaintext key is ever stored on the server: approval records intent, and the key is minted in the same transaction that the client's claim consumes the code.
1. Ask for a code
Unauthenticated. client names the software asking; mode and name describe the key the person will be asked to approve.
curl -s -X POST https://signenvoystaging.fly.dev/v1/device/code \
-H "Content-Type: application/json" \
-d '{"client": "my-tool", "client_version": "1.0.0", "mode": "test", "name": "laptop"}' {
"device_code": "...",
"user_code": "BCDF-GHJK",
"verification_uri": "https://app.signenvoy.com/device",
"verification_uri_complete": "https://app.signenvoy.com/device?code=BCDF-GHJK",
"expires_in": 600,
"interval": 5
}
Show the person user_code and open
verification_uri_complete for them. The code uses letters only,
without vowels or look-alikes, so it survives being read aloud.
2. The person approves
The dashboard page shows exactly what is being authorized before the
button: the requesting client and version, the key's mode (test keys
capture email and watermark sealed PDFs; live keys send real email and
count toward the allowance), the key's name, and the workspace it will
belong to. Any member of a Pro workspace may approve. A Personal
workspace cannot hold API keys; its page says so, and the waiting client
receives device-access-denied.
3. Poll for the key
curl -s -X POST https://signenvoystaging.fly.dev/v1/device/token \
-H "Content-Type: application/json" \
-d '{"device_code": "..."}' Poll every interval seconds. Until the person acts, the answer is a problem:
device-authorization-pending(400): keep polling.device-slow-down(400): you polled too soon; the interval grew by five seconds.device-code-expired(400): the code is unknown, past its ten minutes, or already claimed. Start again.device-access-denied(403): the person declined, or the workspace is Personal.
Once approved, the next poll returns the key exactly once:
{
"api_key": "senv_test_...",
"key_id": "...",
"display": "senv_test_…ab12",
"mode": "test",
"name": "laptop",
"tenant_slug": "acme"
}
Store it where your platform keeps secrets; the dashboard lists it under
API keys like any other, and it can be revoked there or with
POST /v1/keys/{id}/revoke.
Limits
Both routes are unauthenticated and throttled per client address
(sustained 1 request per second, burst 10); past that they answer
rate-limited with
Retry-After. Codes expire after ten minutes.