Skip to content
Trustaige Docs

Sign customers up

The sign-up flow call by call. Prove a person with HumanProof, get a client token, ask for a subject and claim link, and send the link yourself.

View as Markdown

Five steps, three of them yours to write. Before you start, set up the hub.

1. Prove a person on your form

HumanProof keeps automated sign-ups out of your form. Load the script and mark the form. The visitor confirms with a passkey, creating an identity if they have none, and the form gains a short-lived token stating that a person is present. The token carries no identifier.

<script src="https://id.trustaige.com/humanproof.js" defer></script>

<form action="/signup" method="post" data-humanproof>
  <input name="name">
  <input name="email" type="email">
  <!-- the widget draws itself here and adds a humanproof-token field -->
  <button>Create account</button>
</form>
const el = document.getElementById('proof');
HumanProof.render(el, {
  onProof(token) { form.elements.humanProof.value = token; },
  onUnavailable() { /* this browser can't hold a passkey: say so */ },
});
// after a submission spends the token:
HumanProof.reset(el);

Your server verifies the token before it creates anything:

POST /humanproof/verify HTTP/1.1
Host: id.trustaige.com
Content-Type: application/json

{ "origin": "<your-site-host>", "token": "<token from the form>" }
{ "valid": true, "tier": "prove" }

Proceed only on "valid": true; otherwise refuse the submission and ask the person to confirm again. A token is valid once, for the site it was issued to. Where the browser can’t use passkeys, the form can’t be submitted: tell the person so. Every answer is in the verify reference.

<script src="https://id.trustaige.com/humanproof.js" defer></script>

<form action="/signup" method="post" data-humanproof>
  <input name="name">
  <input name="email" type="email">
  <!-- the widget draws itself here and adds a humanproof-token field -->
  <button>Create account</button>
</form>
const el = document.getElementById('proof');
HumanProof.render(el, {
  onProof(token) { form.elements.humanProof.value = token; },
  onUnavailable() { /* this browser can't hold a passkey: say so */ },
});
// after a submission spends the token:
HumanProof.reset(el);
POST /humanproof/verify HTTP/1.1
Host: id.trustaige.com
Content-Type: application/json

{ "origin": "<your-site-host>", "token": "<token from the form>" }
{ "valid": true, "tier": "prove" }

2. Get a client token

On your server, with the client-credentials grant and the same client ID and secret your customers sign in through. Any OAuth library does this and caches the token until it expires.

POST /oauth2/token HTTP/1.1
Host: id.trustaige.com
Authorization: Basic base64(TRUSTAIGE_CLIENT_ID:TRUSTAIGE_CLIENT_SECRET)
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=offers:write issued:read issued:manage
curl -s https://id.trustaige.com/oauth2/token \
  -u "$TRUSTAIGE_CLIENT_ID:$TRUSTAIGE_CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d scope="offers:write issued:read issued:manage"

Request the scopes your calls need, by name; each call says which. A request that names no scope still returns a token, but one that every call refuses with 403.

POST /oauth2/token HTTP/1.1
Host: id.trustaige.com
Authorization: Basic base64(TRUSTAIGE_CLIENT_ID:TRUSTAIGE_CLIENT_SECRET)
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=offers:write issued:read issued:manage
curl -s https://id.trustaige.com/oauth2/token \
  -u "$TRUSTAIGE_CLIENT_ID:$TRUSTAIGE_CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d scope="offers:write issued:read issued:manage"

After verifying the HumanProof token, save your own record, then ask for a subject for the person, with a token that has offers:write. The request names the credential type and, optionally, its claim values and how long the link stays open.

POST /o/<your-org-id>/api/credentials/offers HTTP/1.1
Host: id.trustaige.com
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "type": "Customer",
  "claims": { "tier": "standard" },
  "validForDays": 14
}
POST /o/<your-org-id>/api/credentials/offers HTTP/1.1
Host: id.trustaige.com
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "type": "Customer",
  "subject": "<the subject you stored>"
}
curl -s "$TRUSTAIGE_ISSUER/api/credentials/offers" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"Customer","claims":{"tier":"standard"},"validForDays":14}'
{
  "id": "4f0c…",
  "subject": "b1aa…",
  "state": "open",
  "claimUrl": "https://id.trustaige.com/o/<your-org-id>/credentials/Customer/claim?offer=…",
  "expiresAt": "2026-10-20T09:00:00Z"
}

Store subject against your record, in a unique column, and send the person claimUrl.

claims sets one of the allowed values for each claim your credential type declares, such as tier from standard or gold; claims take no free text. validForDays is how long the claim link stays open: 1 to 30, 7 if you leave it out.

ResponseMeansWhat to do
400 invalid_claimsA claim or value your type doesn’t declareFix your form’s choices
400 invalid_valid_for_daysvalidForDays is outside 1–30Send a value in range, or leave it out
404 unknown_subjectThe subject you re-offered to no longer existsMake a fresh offer and store the new subject
409 already_claimedThe person already has their credentialSend them to sign in
429More than 60 requests a minute from this clientBack off and retry

Every response is in the offer reference.

POST /o/<your-org-id>/api/credentials/offers HTTP/1.1
Host: id.trustaige.com
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "type": "Customer",
  "claims": { "tier": "standard" },
  "validForDays": 14
}
POST /o/<your-org-id>/api/credentials/offers HTTP/1.1
Host: id.trustaige.com
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "type": "Customer",
  "subject": "<the subject you stored>"
}
curl -s "$TRUSTAIGE_ISSUER/api/credentials/offers" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"Customer","claims":{"tier":"standard"},"validForDays":14}'
{
  "id": "4f0c…",
  "subject": "b1aa…",
  "state": "open",
  "claimUrl": "https://id.trustaige.com/o/<your-org-id>/credentials/Customer/claim?offer=…",
  "expiresAt": "2026-10-20T09:00:00Z"
}

Deliver claimUrl in your own welcome email, from your own domain. Trustaige doesn’t send it.

The link works once, for whoever opens it and confirms with a passkey; that person then becomes subject. Because you sent it to the address you hold, their first sign-in proves the address is theirs: mark it verified then.

A link stays open for validForDays and works once. People lose emails, and links expire. When someone signs up again with an address whose account they never finished, don’t ask for a new subject: re-offer to the subject you already hold, by adding subject to the same call (the Re-offer tab above).

  • The subject never changes. You get a fresh link for the same person. Your record stays keyed to the subject you stored the first time.
  • Only the newest link works. Every earlier link for that subject stops working at once, so an old email opened late can’t claim it.
  • Claimed already? Send them to sign in. If the person opened a link in the meantime, you get 409 already_claimed: they have their account.
  • Gone? Start fresh. Subjects nobody claimed are cleared thirty days after their last link closed. A re-offer then answers 404 unknown_subject: make a new offer without subject, and store the new subject in place of the old.

A resend comes through your sign-up form, so the form’s HumanProof check applies to it as to any submission.

5. The person claims

The link opens a Trustaige page showing your credential, with your organization’s name and proved domain. The person confirms with a passkey, or creates one, which takes a few seconds, and saves the credential to their identity. A Continue button then sends them to your initiate login URI, which starts a normal sign-in and lands them in their account with you.

Your initiate login URI must start a sign-in on its own, with no other input: Trustaige sends people there with nothing attached. Usually it is the same route your Sign in button uses, such as /auth/login.

If the person already has an account with you, the claim is refused and they are told to sign in instead. A subject you were given is never quietly replaced by another.

Next: sign people in and out.