Skip to content

Invitations & Notifications

Fabric supports a complete invitation workflow: create invitations, send email notifications, verify tokens, and automatically create memberships on accept.

POST /v1/invitations (Admin+ creates invitation)
|
v
Email sent via Resend (outbox poller delivers)
Webhook fired to org URL (if configured)
|
v
GET /v1/invitations/{id}/accept?token=xxx (invitee clicks link)
|
v
Membership created (invitee joins org with invited role)
Redirect to FABRIC_INVITE_REDIRECT_URL

Requires Admin role or higher in the target organization.

Terminal window
curl -X POST https://gofabric.dev/v1/invitations \
-H 'Authorization: Bearer fab_xxx' \
-H 'content-type: application/json' \
-d '{
"organization_id": "<org-id>",
"email": "alice@example.com",
"role": "member"
}'

Response:

{
"meta": { "status": 201 },
"data": {
"id": "8d464c13-...",
"organization_id": "c1ed629e-...",
"email": "alice@example.com",
"role": "member",
"status": "pending",
"expires_at": "2026-04-09T12:00:00Z"
}
}
GuardHTTP StatusDescription
Duplicate email409 ConflictA pending invitation already exists for this email in this org
Max pending429 Too Many RequestsOrganization has reached max_pending_invitations limit
Max per hour429 Too Many RequestsOrganization has reached max_invitations_per_hour limit
Insufficient role403 ForbiddenCaller lacks Admin+ role or explicit ACL grant

Rate limits are configurable per-org via Organization Settings.

The accept endpoint is a GET (not POST) so it works as a clickable email link:

GET /v1/invitations/{id}/accept?token={raw_token}

Security:

  • The token is a 32-byte random value generated at invitation creation
  • Only the BLAKE3 keyed hash is stored in the database
  • Verification uses constant-time comparison to prevent timing attacks
  • The server-side key is configured via FABRIC_INVITE_TOKEN_KEY

On success:

  1. Invitation status transitions to accepted
  2. A Membership is created linking the invitee to the organization with the invited role
  3. If FABRIC_INVITE_REDIRECT_URL is set, redirects to: {url}?status=accepted&org_id={org_id}&invitation_id={id}
  4. Otherwise returns a JSON response

On failure:

  • Invalid/missing token: ?status=error&reason=invalid_token
  • Already accepted: ?status=error&reason=already_accepted
  • Expired: ?status=error&reason=expired
Terminal window
# List all invitations for an org
curl 'https://gofabric.dev/v1/invitations?organization_id=<org-id>' \
-H 'Authorization: Bearer fab_xxx'
# Get a single invitation
curl https://gofabric.dev/v1/invitations/<invitation-id> \
-H 'Authorization: Bearer fab_xxx'
Terminal window
curl -X POST https://gofabric.dev/v1/invitations/<id>/revoke \
-H 'Authorization: Bearer fab_xxx'

Revoked invitations cannot be accepted.

Invitations expire after 7 days. A background job runs every 10 minutes to mark expired invitations. Expired invitations cannot be accepted — the inviter must create a new one.

When an invitation is created, Fabric queues an email notification via a transactional outbox:

  1. An outbox entry is written to the database in the same request
  2. A background poller (every 10 seconds) claims pending entries and sends them via Resend
  3. Failed sends are retried with exponential backoff: 1 minute, 5 minutes, 30 minutes (3 attempts max)

The invitation email includes:

  • Organization display name (configurable via org settings)
  • Invited role
  • A clickable “Accept Invitation” button linking to the Fabric accept endpoint
  • Expiry notice (7 days)
VariableRequiredDescription
RESEND_API_KEYYesResend API key (starts with re_)
FABRIC_EMAIL_FROMNoSender address (default: noreply@gofabric.dev)
FABRIC_INVITE_TOKEN_KEYNo32-byte hex key for token hashing (auto-generated if unset)
FABRIC_INVITE_REDIRECT_URLNoURL to redirect to after accept

Domain verification: Add your sending domain to Resend and configure the required DNS records (DKIM, SPF, DMARC).

Invitation events (invitation.created, invitation.accepted, invitation.revoked) are delivered automatically to any webhook subscriptions registered for the organization. Register a webhook filtered on these events to receive push notifications:

await fabric.webhooks.create(orgId, {
url: "https://your-app.com/webhooks/fabric",
secret: process.env.FABRIC_WEBHOOK_SECRET,
event_filter: ["invitation.created", "invitation.accepted", "invitation.revoked"],
});

See the Webhooks Reference for signature verification, delivery semantics, and the full event catalog.

Per-org configuration for branding and rate limits:

Terminal window
# Get settings (returns defaults if none configured)
curl https://gofabric.dev/v1/organizations/<org-id>/settings \
-H 'Authorization: Bearer fab_xxx'
# Update settings
curl -X PUT https://gofabric.dev/v1/organizations/<org-id>/settings \
-H 'Authorization: Bearer fab_xxx' \
-H 'content-type: application/json' \
-d '{
"display_name": "Acme Corp",
"logo_url": "https://acme.com/logo.png",
"email_from_name": "Acme Corp via Fabric",
"max_pending_invitations": 50,
"max_invitations_per_hour": 10
}'
FieldDefaultDescription
display_nameorg slugName shown in invitation emails
logo_urlnoneLogo URL for email headers
email_from_namenoneCustom “From” name (e.g., “Acme Corp via Fabric”)
max_pending_invitations100Max concurrent pending invitations per org
max_invitations_per_hour20Max new invitations created per hour per org
MethodPathDescription
POST/v1/invitationsCreate invitation (Admin+)
GET/v1/invitationsList invitations (requires organization_id query param)
GET/v1/invitations/{id}Get invitation by ID
GET/v1/invitations/{id}/acceptAccept invitation (token required)
POST/v1/invitations/{id}/revokeRevoke invitation
GET/v1/organizations/{id}/settingsGet org settings
PUT/v1/organizations/{id}/settingsUpdate org settings (Owner)