Server integration
Add phone binding and confirmation to your website with API parameter references and official SDK examples.
The AuthLink production API is https://api.getauthlink.com. Use your own API for the application backend; the AuthLink website domain is not an API endpoint.
Integration flow and trust boundaries
- The operator creates a separate tenant for your website and delivers its credentials to your backend.
- The backend obtains a stable user ID from the signed-in session and generates random state, a creation key and a separate consumption key.
- Create a bind / confirm request, save the user, session, purpose, state, requestId, expiresAt and keys, then return only the public ticket to the browser.
- The user checks the website, account and code in AuthLink, then approves the request.
- On approved / consumed, the backend calls consume using the saved state and consumption key.
- Validate the receipt and persist a unique requestId / verificationId record in your own database transaction before returning confirmed. Protected business endpoints check that record.
There is no webhook.approved, six-digit codes and frontend callbacks cannot directly authorize business actions. AuthLink does not receive action, amounts, transactionHash or permission scopes.
Tenants and authentication
Obtain your site’s tenantId, apiKey, origin and expiresAt from the operator. New keys never expire; the operator can still rotate or revoke keys or disable the tenant. Existing keys keep their original expiry. Store keys securely on your backend only.
| Header | Requirement |
|---|---|
Authorization
|
Bearer <tenant API key>; required for all integration endpoints |
Content-Type
|
POST uses application/json; unknown fields and duplicate JSON keys are rejected |
Idempotency-Key
|
Required for creation and consumption: 16–128 letters, digits, underscores or hyphens. Prefer 32 random bytes in Base64URL and reuse the key on retries. |
Origin / Cookie
|
The API rejects browser Origin and Cookie headers; browsers connect through their own website backend. |
Create requests and parameters
POST /v1/integration/requests · API origin:
{
"externalUserId": "stable-user-id",
"accountLabel": "Account display name",
"purpose": "bind",
"state": "REPLACE_WITH_SAVED_43_CHARACTER_RANDOM_STATE"
}
| Field | Type / required | Description |
|---|---|---|
externalUserId
|
string / required | 1–128 printable ASCII bytes (0x21–0x7e). Take it from the signed-in session; never recycle it or let the browser choose it. |
accountLabel
|
string / required | Nonempty, up to 160 UTF-8 bytes; control characters and specified bidi control characters are rejected. |
purpose
|
string / required | First binding: bind; confirmation after binding: confirm
|
state
|
string / required | 32 secure random bytes encoded as unpadded Base64URL, 43 characters. Save it on the backend and compare it unchanged at consumption. |
Success returns HTTP 200 and a ticket:
| Response field | Meaning |
|---|---|
requestId
|
43-character request identifier |
purpose / expiresAt
|
Request purpose and UTC ISO expiry time; lifetime is 120 seconds |
qr
|
QR JSON string: v, origin, requestId, token. Deliver only to the frontend component or app; never log it. |
qrSvg
|
Compatibility field; the official web package generates its own QR and does not insert this field through innerHTML |
pairingCode
|
Current six-digit string; preserve leading zeros |
displayCode
|
value、serverTime、validUntil、periodSeconds:30 |
A new request for the same account rejects its unfinished requests. Code refreshes do not extend expiry or require a new request.
Status, consumption and account APIs
| Method / path | Input | Response / notes |
|---|---|---|
GET /v1/integration/requests/{id}
|
Tenant bearer | requestId, status, expiresAt, optional displayCode. Status: new / pending / approved / consumed / rejected / expired |
POST /v1/integration/requests/{id}/consume
|
Tenant bearer, Idempotency-Key; body {"state":"saved-43-character-state"}
|
Consume after approval to receive confirmed. Reuse the saved consumption key when the network outcome is uncertain. |
POST /v1/integration/accounts/status
|
Tenant bearer; body {"externalUserId":"..."}
|
externalUserId, status:unbound/bound/deleted, platform (null when unbound) |
POST /v1/integration/accounts/delete
|
Tenant bearer; body {"externalUserId":"..."}
|
{"status":"deleted"}; permanently revokes the AuthLink binding for this site, with no rebinding. Does not delete the CMS user. |
| Consumption receipt field | Validation |
|---|---|
status / authenticationPolicy
|
status must be confirmed; the official SDK validates authenticationPolicy |
requestId / tenantId / externalUserId
|
Must match the saved request, tenant, signed-in user and session |
purpose / state / expiresAt
|
Must exactly match saved backend values and remain unexpired |
verificationId / platform / consumedAt
|
Validate the result ID, android / ios and timestamps; persist with a unique constraint |
Go SDK and methods
Independent module authlink.local/sdk, Go 1.22+, standard library only. For Go website backends, without importing the AuthLink server or MySQL driver.
Download Go SDK source · Read the complete example →
Install and initialize
Keep the SDK’s own go.mod after extracting it. Add only the SDK module to your website project.
The SDK ships as source and has no public Go module release yet. Extract it in your website project and use a local replace directive:
mkdir -p third_party
tar -xzf authlink-go.tar.gz -C third_party
go mod edit -replace=authlink.local/sdk=./third_party/authlink-go
go mod edit -require=authlink.local/[email protected]
go mod tidy
import authlink "authlink.local/sdk"
client, err := authlink.New(authlink.Config{
Origin: "https://api.getauthlink.com", // API origin, not the marketing site
TenantID: os.Getenv("AUTHLINK_TENANT_ID"),
APIKey: os.Getenv("AUTHLINK_API_KEY"),
})
if err != nil { return err }
// Get userID and label from your authenticated website session.
state, err := authlink.RandomToken()
if err != nil { return err }
createKey, err := authlink.RandomToken()
if err != nil { return err }
consumeKey, err := authlink.RandomToken()
if err != nil { return err }
// Persist state, both keys and user/session first. Reuse them on retries.
ticket, err := client.CreateRequest(ctx, authlink.CreateInput{
ExternalUserID: userID, AccountLabel: label,
Purpose: "bind", State: state,
}, createKey)
if err != nil { return err }
expected := authlink.Expected{
RequestID: ticket.RequestID, ExternalUserID: userID,
Purpose: ticket.Purpose, State: state, ExpiresAt: ticket.ExpiresAt,
}
// Save expected and consumeKey before returning the public ticket.
// Load saved expected / consumeKey after checking session, CSRF and ownership.
status, err := client.Status(ctx, expected.RequestID)
if err != nil { return err }
if status.Status == "approved" || status.Status == "consumed" {
result, err := client.Consume(ctx, expected, consumeKey)
if err != nil { return err }
// The SDK has validated the full receipt. Persist a unique confirmation
// in your own transaction, associated with this user/session and business.
_ = result
}
| Config options | Description |
|---|---|
Origin / TenantID / APIKey | Required: API HTTPS origin without a trailing slash, tenant ID and private backend key |
AllowLocalHTTP | Defaults to false; only loopback / localhost development is allowed |
HTTPClient | Optional *http.Client. The SDK copies it, rejects redirects and disables CookieJar. Use a trusted transport. Each request is capped at 10 seconds; an earlier context deadline or shorter client timeout takes precedence. |
| Method | Return value and purpose |
|---|---|
CreateRequest(ctx, CreateInput, key) | (Ticket, error). CreateInput: ExternalUserID, AccountLabel, Purpose, State. Purpose defaults to confirm; use bind for first binding. |
Status(ctx, requestID) | (RequestStatus, error). Reads status and current displayCode; does not authorize business actions. |
Consume(ctx, Expected, key) | (Confirmation, error). Consumes and verifies the policy, tenant, user, request, purpose, state, expiry, result ID and platform. |
WaitForConfirmation(ctx, Expected, key, interval) | (Confirmation, error). Optional backend polling; consumes approved / consumed results. interval=0 defaults to 1500ms; allowed range is 100ms–10s. |
AccountStatus(ctx, externalUserID) | (AccountStatus, error). Reads unbound / bound / deleted; Platform is a nullable *string. |
DeleteAccount(ctx, externalUserID) | error. Permanently revokes this site's AuthLink binding, preventing rebinding; does not delete the CMS user. |
RandomToken() | (string, error). 32 secure random bytes in Base64URL. Generate and save separate values for state, creation and consumption. |
ValidateConfirmation(result, expected, tenantID, now) | error. Field validation only, not signature verification. Never trust a browser-supplied receipt as authorization. |
Expected contains RequestID, ExternalUserID, Purpose, State and ExpiresAt. Load it from the saved backend request and preserve the exact expiresAt string. Response structs have protocol JSON tags for the official web components. Unknown fields, duplicate keys and case aliases are rejected; update the SDK when protocol fields change.
Waiting, cancellation and errors
result, err := client.WaitForConfirmation(ctx, expected, consumeKey, 0)
switch {
case err == nil:
// Persist result in your own transaction before updating UI or authorizing work.
_ = result
case errors.Is(err, authlink.ErrRejected):
// The user rejected the request.
case errors.Is(err, authlink.ErrExpired):
// The request expired.
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
// This call was canceled or timed out; the platform request is not revoked.
default:
var api *authlink.APIError
if errors.As(err, &api) {
// Handle api.StatusCode / api.Code; retry with the same consumption key.
}
}
Complete Go example
The example has two stages: create creates a request; result queries and consumes the saved request. Run result only after phone approval. The example does not implement website sessions or database persistence and does not authorize business actions.
Expand or collapse the complete Go example
// This example creates a request, then queries/consumes a previously saved one.
// Supply tenant credentials only through your backend's private environment.
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"time"
authlink "authlink.local/sdk"
)
func run() error {
mode := flag.String("mode", "create", "create or result")
user := flag.String("user", "", "stable user ID from your authenticated session")
label := flag.String("label", "", "account display label")
purpose := flag.String("purpose", "bind", "bind or confirm")
flag.Parse()
client, err := authlink.New(authlink.Config{Origin: os.Getenv("AUTHLINK_ORIGIN"), TenantID: os.Getenv("AUTHLINK_TENANT_ID"), APIKey: os.Getenv("AUTHLINK_API_KEY")})
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
switch *mode {
case "create":
// For this standalone example, supply already-generated and saved values
// through the environment. A website must persist them before networking.
ticket, err := client.CreateRequest(ctx, authlink.CreateInput{ExternalUserID: *user, AccountLabel: *label, Purpose: *purpose, State: os.Getenv("AUTHLINK_STATE")}, os.Getenv("AUTHLINK_CREATE_KEY"))
if err != nil {
return err
}
// Contains QR token: deliver to the intended user's UI; never log it.
return json.NewEncoder(os.Stdout).Encode(ticket)
case "result":
expected := authlink.Expected{RequestID: os.Getenv("AUTHLINK_REQUEST_ID"), ExternalUserID: *user, Purpose: *purpose, State: os.Getenv("AUTHLINK_STATE"), ExpiresAt: os.Getenv("AUTHLINK_EXPIRES_AT")}
status, err := client.Status(ctx, expected.RequestID)
if err != nil {
return err
}
if status.Status != "approved" && status.Status != "consumed" {
return json.NewEncoder(os.Stdout).Encode(status)
}
result, err := client.Consume(ctx, expected, os.Getenv("AUTHLINK_CONSUME_KEY"))
if err != nil {
return err
}
// In a website, persist result atomically with unique request/result IDs,
// user/session and business association before returning confirmed.
_ = result
fmt.Fprintln(os.Stderr, "Receipt validated; this example does not persist or authorize business.")
return nil
default:
return errors.New("mode must be create or result")
}
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Command-line options
| Parameter | Purpose |
|---|---|
-mode | create creates a request; result queries and consumes it. Default: create. |
-user | The signed-in website user’s stable ID, obtained from the backend session. |
-label | The account label displayed when creating a request. |
-purpose | Use bind for initial binding and confirm for later confirmations. This example defaults to bind. |
Backend environment variables
| Variable | Purpose |
|---|---|
AUTHLINK_ORIGIN / AUTHLINK_TENANT_ID / AUTHLINK_API_KEY | The API origin, tenant ID and private key supplied by the operator. Keep them on the website backend. |
AUTHLINK_STATE / AUTHLINK_CREATE_KEY / AUTHLINK_CONSUME_KEY | Generate and persist separate state, creation and consumption keys on your backend. Reuse them on retries. |
AUTHLINK_REQUEST_ID / AUTHLINK_EXPIRES_AT | For result, load the request ID and original expiry from the ticket saved on your backend. |
The SDK archive includes this example at examples/quickstart/main.go. You can also copy the code above into main.go. Supply credentials through the backend environment, never in source code or the browser.
# Run from a Go project that already includes the SDK module.
# Supply the private environment variables listed above before running.
export AUTHLINK_ORIGIN=https://api.getauthlink.com
go run . -mode create -user stable-user-id -label "Example account" -purpose bind
# After phone approval, load the SAME saved request context before running:
go run . -mode result -user stable-user-id -purpose bind
Node SDK and methods
// Copy /sdk/node/ and /sdk/browser/ with their relative paths. Server import only.
import { AuthLink, randomToken } from './sdk/node/authlink.mjs';
const auth = new AuthLink({
origin: 'https://api.getauthlink.com',
tenantId: process.env.AUTHLINK_TENANT_ID,
apiKey: process.env.AUTHLINK_API_KEY,
});
// user must come from your authenticated session. This is the creation stage.
const state = randomToken();
const createKey = randomToken();
const consumeKey = randomToken();
// Persist these values and user/session before networking. Reuse on retries.
const ticket = await auth.createRequest({
externalUserId: user.id, accountLabel: user.displayName,
purpose: 'bind', state,
}, createKey);
const expected = { requestId: ticket.requestId, expiresAt: ticket.expiresAt,
externalUserId: user.id, purpose: 'bind', state };
// Save expected, consumeKey and session association before returning ticket.
// Your result endpoint loads the saved request context:
const status = await auth.status(expected.requestId);
if (['approved', 'consumed'].includes(status.status)) {
const verified = await auth.consume(expected, consumeKey);
// The SDK has verified the receipt. Persist a unique confirmation atomically.
// Return verified to the browser only after the database commit succeeds.
}
| Constructor option / method | Description |
|---|---|
origin / tenantId / apiKey
|
Required: API HTTPS origin without a trailing slash, tenant ID and private key |
allowLocalHttp
|
Defaults to false; loopback development only |
createRequest(input, key)
|
Create a request; see fields above. Purpose defaults to confirm. |
status(requestId, signal?)
|
Read status; signal is an optional AbortSignal |
consume(expected, key, signal?)
|
Consume and validate the receipt, returning a verified result |
waitForConfirmation(expected, options)
|
Backend polling helper. options.idempotencyKey and onConfirmed are required; optional onRejected / onExpired, signal and intervalMs (default 1500, range 100–10000ms). Your application must still persist the result. |
accountStatus(id) / deleteAccount(id)
|
Read / permanently revoke the tenant's account binding |
Installation, method options and usage examples are on this page. The Node SDK and its dependency are in one archive. Preserve the relative node and browser directories after extraction; install web components through npm.
mkdir -p sdk
tar -xzf authlink-node.tar.gz -C sdk
# Import ./sdk/node/authlink.mjs from your backend project.
Error handling
| HTTP | Common code | Action |
|---|---|---|
| 400 | INVALID_REQUEST / INVALID_SUBJECT / INVALID_DISPLAY_TEXT / INVALID_IDEMPOTENCY_KEY | Correct the input constraints. Do not create new requests to hide an uncertain outcome. |
| 401 | UNAUTHORIZED / STATE_MISMATCH / SERVER_CREDENTIAL_REQUIRED | Check private credentials, state, Host and whether the website backend was bypassed. |
| 404 | NOT_FOUND | Request or release channel does not exist |
| 409 | APPROVAL_REQUIRED / REQUEST_EXPIRED / REQUEST_USED / IDEMPOTENCY_CONFLICT / ACCOUNT_ALREADY_BOUND / ACCOUNT_NOT_BOUND | Handle the business state; ask the user to retry after expiry and preserve idempotent recovery. |
| 429 | RATE_LIMITED / BUSY | Back off and retry with the original idempotency key |
| 503 | ATTESTATION_UNAVAILABLE | The verification service is temporarily unavailable. Try again later. |
Error body: {"code":"..."}. Never treat unknown errors as success. Do not share-cache API responses or log bearer keys, QR tokens, complete proofs or raw secrets.