# 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.

Source: https://docs.trustaige.com/customer-credentials/sign-up

Replace these placeholders with your organization's own values:

- `<your-org-id>`: Organization short ID, for example `a1B2c3D4e5F6`. The part after /o/ in your hub’s address.
- `<your-site-host>`: Your site’s host, for example `www.example.com`. The host your form lives on, as you registered it under HumanProof → Sites.

Five steps, three of them yours to write. Before you start, [set up the hub](/customer-credentials/setup).

## 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.

```html
<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>
```

```js
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:

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

{ "origin": "<your-site-host>", "token": "<token from the form>" }
```

```json
{ "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](/reference/verify-humanproof).

## 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.

```http
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
```

```bash
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`.

## 3. Ask for a subject and a claim link

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.

```http
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
}
```

```http
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>"
}
```

```bash
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}'
```

```json title="201 Created"
{
  "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`.

> **What the subject is:** `subject` is the OpenID Connect subject identifier for this person in your
> organization: every ID token issued to your apps for them carries it as `sub`.
> It is scoped to your organization like a pairwise identifier ([OpenID Connect
> Core 1.0, §8](https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes)):
> the same for all of your organization's apps, stable for the life of the
> relationship, and not correlatable with the value any other organization
> receives for the same person. Make it the unique key of your customer record.

`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.

| Response | Means | What to do |
|---|---|---|
| `400 invalid_claims` | A claim or value your type doesn't declare | Fix your form's choices |
| `400 invalid_valid_for_days` | `validForDays` is outside 1–30 | Send a value in range, or leave it out |
| `404 unknown_subject` | The subject you re-offered to no longer exists | Make a fresh offer and store the new subject |
| `409 already_claimed` | The person already has their credential | Send them to sign in |
| `429` | More than 60 requests a minute from this client | Back off and retry |

Every response is in the [offer reference](/reference/create-offer).

## 4. Send the link yourself

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.

### When a link is lost or expires

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.

> **Never replace a subject while its link may still work:** Asking for a brand-new subject on every resend leaves the earlier link alive.
> If the person opens the old email, they claim a subject you no longer hold, and
> their sign-in comes back with a `sub` you don't recognise. Re-offering keeps one
> subject per person and only one working link.

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](/customer-credentials/sign-in).
