Skip to content

Account Invitations

An invitation brings someone into an account by email address, before they necessarily have a Chainabit user account of their own. This is different from Account Members, where POST /accounts/:accountId/members attaches someone who already has a user ID -- an invitation exists precisely because the account owner may only know an email address, and the person on the other end might sign up for the first time in order to accept it.

Nothing is added to the member roster until the invitation is accepted. Sending, resending, and revoking an invitation never creates or touches a membership row.

Account Invitations

GET /accounts/:accountId/invitations

List invitations for an account. Requires owner or admin role.

Authentication: JWT Bearer token + owner/admin role required

Request

ParameterTypeRequiredDescription
limitnumberNoPage size, up to 100 (default 50)
offsetnumberNoNumber of results to skip

Response

Response Example
json
{
  "data": [
    {
      "id": "inv-0001",
      "accountId": "acc-1234-5678",
      "email": "carol@example.com",
      "role": "member",
      "status": "invited",
      "invitedBy": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "expiresAt": "2026-03-24T11:00:00.000Z",
      "acceptedAt": null,
      "revokedAt": null,
      "createdAt": "2026-03-17T11:00:00.000Z"
    }
  ],
  "meta": {
    "total": 1
  },
  "error": null
}
Response Fields
FieldTypeDescription
idstringInvitation ID
accountIdstringAccount this invitation belongs to
emailstringInvited email address
rolestringRole that will be assigned when accepted (admin, analyst, billing, viewer, or member)
statusstringinvited, accepted, revoked, or expired -- derived from the timestamps below, not stored directly
invitedBystringUser ID of whoever sent the invitation
expiresAtstringISO 8601 timestamp after which the invitation can no longer be accepted
acceptedAtstring | nullISO 8601 timestamp of acceptance, null while pending
revokedAtstring | nullISO 8601 timestamp of revocation, null unless withdrawn
createdAtstringISO 8601 timestamp the invitation was created

Note there is no token field here -- see POST /accounts/:accountId/invitations for why.

Code Examples

bash
curl "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID/invitations" \
  -H "Authorization: Bearer $TOKEN"
javascript
const response = await fetch(`${BASE_URL}/accounts/${ACCOUNT_ID}/invitations`, {
  headers: {
    Authorization: `Bearer ${TOKEN}`,
  },
});
const data = await response.json();
python
import requests

response = requests.get(
    f"{BASE_URL}/accounts/{ACCOUNT_ID}/invitations",
    headers={"Authorization": f"Bearer {TOKEN}"},
)
data = response.json()

POST /accounts/:accountId/invitations

Invite an email address to the account. Requires owner or admin role.

The response never includes the raw invitation token or link. The token is a one-time credential for whoever can read the invitee's mailbox -- not for the admin who typed the address, and not for any request log, browser history, or analytics pipeline the response happens to pass through. The invitation email itself carries the link; if it's lost, resend it rather than looking up the original token.

Authentication: JWT Bearer token + owner/admin role required

Request

FieldTypeRequiredConstraintsDescription
emailstringYesValid email, max 255 charsEmail address to invite
rolestringNoadmin, analyst, billing, viewer, or member -- defaults to memberRole to assign once accepted

Response

Response Example
json
{
  "data": {
    "id": "inv-0001",
    "accountId": "acc-1234-5678",
    "email": "carol@example.com",
    "role": "member",
    "status": "invited",
    "invitedBy": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "expiresAt": "2026-03-24T11:00:00.000Z",
    "acceptedAt": null,
    "revokedAt": null,
    "createdAt": "2026-03-17T11:00:00.000Z"
  },
  "meta": null,
  "error": null
}
Response Fields

Same shape as the invitation object above (no token).

Code Examples

bash
curl -X POST "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID/invitations" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "carol@example.com",
    "role": "member"
  }'
javascript
const response = await fetch(`${BASE_URL}/accounts/${ACCOUNT_ID}/invitations`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    email: "carol@example.com",
    role: "member",
  }),
});
const data = await response.json();
python
import requests

response = requests.post(
    f"{BASE_URL}/accounts/{ACCOUNT_ID}/invitations",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={"email": "carol@example.com", "role": "member"},
)
data = response.json()

POST /accounts/:accountId/invitations/:invitationId/resend

Re-send an invitation with a fresh link. The previous link stops working the moment this is called. Requires owner or admin role.

Authentication: JWT Bearer token + owner/admin role required

Request

No body. invitationId must be a valid UUID.

Response

Response Example
json
{
  "data": {
    "id": "inv-0001",
    "accountId": "acc-1234-5678",
    "email": "carol@example.com",
    "role": "member",
    "status": "invited",
    "invitedBy": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "expiresAt": "2026-03-31T11:00:00.000Z",
    "acceptedAt": null,
    "revokedAt": null,
    "createdAt": "2026-03-17T11:00:00.000Z"
  },
  "meta": null,
  "error": null
}
Response Fields

Same shape as the invitation object above -- note the refreshed expiresAt.

Code Examples

bash
curl -X POST "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID/invitations/$INVITATION_ID/resend" \
  -H "Authorization: Bearer $TOKEN"
javascript
const response = await fetch(
  `${BASE_URL}/accounts/${ACCOUNT_ID}/invitations/${INVITATION_ID}/resend`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${TOKEN}`,
    },
  }
);
const data = await response.json();
python
import requests

response = requests.post(
    f"{BASE_URL}/accounts/{ACCOUNT_ID}/invitations/{INVITATION_ID}/resend",
    headers={"Authorization": f"Bearer {TOKEN}"},
)
data = response.json()

DELETE /accounts/:accountId/invitations/:invitationId

Withdraw a pending invitation. Requires owner or admin role.

Authentication: JWT Bearer token + owner/admin role required

Request

No body. invitationId must be a valid UUID.

Response

Response Example
json
{
  "data": {
    "id": "inv-0001",
    "accountId": "acc-1234-5678",
    "email": "carol@example.com",
    "role": "member",
    "status": "revoked",
    "invitedBy": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "expiresAt": "2026-03-24T11:00:00.000Z",
    "acceptedAt": null,
    "revokedAt": "2026-03-18T09:00:00.000Z",
    "createdAt": "2026-03-17T11:00:00.000Z"
  },
  "meta": null,
  "error": null
}
Response Fields

Same shape as the invitation object above.

Code Examples

bash
curl -X DELETE "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID/invitations/$INVITATION_ID" \
  -H "Authorization: Bearer $TOKEN"
javascript
const response = await fetch(
  `${BASE_URL}/accounts/${ACCOUNT_ID}/invitations/${INVITATION_ID}`,
  {
    method: "DELETE",
    headers: {
      Authorization: `Bearer ${TOKEN}`,
    },
  }
);
const data = await response.json();
python
import requests

response = requests.delete(
    f"{BASE_URL}/accounts/{ACCOUNT_ID}/invitations/{INVITATION_ID}",
    headers={"Authorization": f"Bearer {TOKEN}"},
)
data = response.json()

Accepting an Invitation

POST /invitations/accept

Accept an invitation as the authenticated user. This route is deliberately not scoped under /accounts/:accountId -- whoever is accepting is, by definition, not yet a member of that account, so an account-membership check would reject every legitimate acceptance.

The caller must still be signed in: the invitation token names an email address, not an identity, and the account needs to learn which signed-in user redeemed it. In practice this covers both cases a product needs -- someone who already has a Chainabit account signs in and posts the token; someone brand new signs up first and then posts the same token.

Authentication: JWT Bearer token

Request

FieldTypeRequiredConstraintsDescription
tokenstringYes64-character hex stringThe token from the invitation link

Response

Response Example
json
{
  "data": {
    "invitation": {
      "id": "inv-0001",
      "accountId": "acc-1234-5678",
      "email": "carol@example.com",
      "role": "member",
      "status": "accepted",
      "invitedBy": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "expiresAt": "2026-03-24T11:00:00.000Z",
      "acceptedAt": "2026-03-18T09:05:00.000Z",
      "revokedAt": null,
      "createdAt": "2026-03-17T11:00:00.000Z"
    },
    "member": {
      "accountId": "acc-1234-5678",
      "chainerId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
      "role": "member",
      "status": "active",
      "email": "carol@example.com",
      "fullName": "Carol Nguyen",
      "username": "carol_n",
      "avatarUrl": null,
      "invitedBy": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "joinedAt": "2026-03-18T09:05:00.000Z"
    },
    "seatConsumed": true
  },
  "meta": null,
  "error": null
}
Response Fields
FieldTypeDescription
invitationobjectThe invitation, now in accepted status
memberobjectThe account membership that was created -- same shape as GET /accounts/:accountId/members
seatConsumedbooleanWhether accepting this invitation took a seat from the account's subscription. false when the invitee was already a member of the account through another path

Code Examples

bash
curl -X POST "https://api.chainabit.com/api/v1/invitations/accept" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "3f9a1c2b8e7d4650a1b2c3d4e5f6a7b83f9a1c2b8e7d4650a1b2c3d4e5f6a7b8"
  }'
javascript
const response = await fetch(`${BASE_URL}/invitations/accept`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    token: "3f9a1c2b8e7d4650a1b2c3d4e5f6a7b83f9a1c2b8e7d4650a1b2c3d4e5f6a7b8",
  }),
});
const data = await response.json();
python
import requests

response = requests.post(
    f"{BASE_URL}/invitations/accept",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={"token": "3f9a1c2b8e7d4650a1b2c3d4e5f6a7b83f9a1c2b8e7d4650a1b2c3d4e5f6a7b8"},
)
data = response.json()

Notes

  • Invitation status is derived from timestamps at read time, not stored as its own column: a live invitation reports invited, and lapses to expired the moment expiresAt passes, with no background job required to make that happen.
  • Resending invalidates the previous link immediately -- there is never more than one valid link for a given invitation.
  • Revoking is only meaningful for a pending invitation; an already-accepted or already-expired invitation has nothing left to withdraw.
  • Seat limits apply the same way they do for direct member additions: if the account is at its seat limit, accepting an invitation can fail even though sending it succeeded, since seat consumption is only known for certain at accept time.

Built with purpose.