Skip to content

Invite a partner into the household (admin only)

POST
/invites
curl --request POST \
--url https://api.tovarifinancial.com/invites \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "email": "hello@example.com" }'

A household ADMIN invites one person, by email, into their household. The server stores only a hash of the invite token and emails the accept link to the invitee — that email is the ONLY place the link ever goes (KAN-3109): acceptUrl is ALWAYS null in this response, on a create and on a replay. It is kept, nullable, so the response shape is unchanged for existing clients; a client must not build, expect or offer a copyable link. emailSent reports whether the email provider accepted the invite email; when it is false, delivery could not be confirmed and the admin should revoke the invite and send it again.

Idempotency: X-Idempotency-Key is optional. When it matches an invite this household already created with that key, the answer is 200 with THAT invite as it stands now (its status may since have moved on), emailSent: false, and nothing is written or re-sent — a drifted body is never applied. A first-time create answers 201. IDEMPOTENCY_CONFLICT is not reachable on this operation.

Refusals, in the order they are checked: 403 FORBIDDEN for a caller who is not a household admin; 409 when the address already belongs to an active member, an invite is already pending for it, or the household is full; 429 INVITE_SEND_THROTTLED when the household has used its daily invite sends. A 403 DATABASE_ERROR is also possible if the row-level policy itself refuses the write — a code the 403 schema below does not enumerate.

X-Idempotency-Key
string

Optional idempotency key for safe write retries. Every operation that declares this header reads it. Three cases, told apart per operation: (1) where the operation also documents a 409 whose codes include IDEMPOTENCY_CONFLICT, replaying this key with a different payload returns 409 IDEMPOTENCY_CONFLICT and replaying it with the same payload returns the original response; (2) where it documents no 409, a replay is reconciled optimistically against a unique key column and answers the original 200; (3) where it documents a 409 whose codes do NOT include IDEMPOTENCY_CONFLICT, a conflict on this key is impossible — a replay answers a 200, and that 409 is a resource-state conflict described by the operation’s own 409 schema.

Media typeapplication/json
object
email
required

The invitee’s address. Trimmed and lower-cased by the server before it is validated and stored.

string format: email
Examplegenerated
{
"email": "hello@example.com"
}

Replay of an X-Idempotency-Key this household already used — the existing invite, nothing re-sent

Media typeapplication/json
object
data
required
object
inviteId
required
string format: uuid
email
required

The invitee address, lower-cased.

string
status
required

Always pending on a 201. A 200 replay returns the invite as it stands now, which may have been accepted or revoked since.

string
Allowed values: pending accepted revoked
expiresAt
required
string format: date-time
acceptUrl
required

ALWAYS null (KAN-3109). The raw invite token leaves the server only inside the invite email, so this response never carries an accept link. Kept, nullable, so the response shape is unchanged for existing clients.

string
nullable
emailSent
required

Whether the email provider accepted the invite email. Always false on a 200 replay (nothing is re-sent) and whenever sending is disabled or failed.

boolean
message
string
requestId
required
string
timestamp
required
string format: date-time
Example
{
"data": {
"status": "pending",
"acceptUrl": null
}
}

Invite created (the invite email is attempted after the create commits)

Media typeapplication/json
object
data
required
object
inviteId
required
string format: uuid
email
required

The invitee address, lower-cased.

string
status
required

Always pending on a 201. A 200 replay returns the invite as it stands now, which may have been accepted or revoked since.

string
Allowed values: pending accepted revoked
expiresAt
required
string format: date-time
acceptUrl
required

ALWAYS null (KAN-3109). The raw invite token leaves the server only inside the invite email, so this response never carries an accept link. Kept, nullable, so the response shape is unchanged for existing clients.

string
nullable
emailSent
required

Whether the email provider accepted the invite email. Always false on a 200 replay (nothing is re-sent) and whenever sending is disabled or failed.

boolean
message
string
requestId
required
string
timestamp
required
string format: date-time
Example
{
"data": {
"status": "pending",
"acceptUrl": null
}
}

Validation failed — see errorCode / fieldErrors

Media typeapplication/json
object
errorCode
required

Machine-readable error code (AUTH_ERROR_CODES).

string
Allowed values: INVALID_EMAIL_FORMAT USER_ALREADY_EXISTS INVALID_INVITE_CODE WRONG_PASSWORD USER_NOT_FOUND AUTHENTICATION_FAILED EXPIRED_CODE INVALID_CODE INVALID_PASSWORD VALIDATION_ERROR SIGNUP_FAILED DATABASE_ERROR SERVICE_UNAVAILABLE PLAID_ERROR IDEMPOTENCY_CONFLICT ACCOUNT_ALREADY_LINKED NOT_FOUND
message
required
string
fieldErrors

Optional field-level validation errors, keyed by field name.

object
key
additional properties
Array<string>
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "INVALID_EMAIL_FORMAT"
}

Missing, invalid, or expired bearer token

Media typeapplication/json
object
errorCode
required

Machine-readable error code (AUTH_ERROR_CODES).

string
Allowed values: INVALID_EMAIL_FORMAT USER_ALREADY_EXISTS INVALID_INVITE_CODE WRONG_PASSWORD USER_NOT_FOUND AUTHENTICATION_FAILED EXPIRED_CODE INVALID_CODE INVALID_PASSWORD VALIDATION_ERROR SIGNUP_FAILED DATABASE_ERROR SERVICE_UNAVAILABLE PLAID_ERROR IDEMPOTENCY_CONFLICT ACCOUNT_ALREADY_LINKED NOT_FOUND
message
required
string
fieldErrors

Optional field-level validation errors, keyed by field name.

object
key
additional properties
Array<string>
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "INVALID_EMAIL_FORMAT"
}

Authenticated, but the tenant gate refuses the request until the caller resolves a precondition. errorCode is one of EMAIL_NOT_VERIFIED, POLICY_ACCEPTANCE_REQUIRED, or SUBSCRIPTION_REQUIRED, evaluated in exactly that order (contract term CCR-1: email verification first, then policy acceptance, then subscription — so a subscriber who has merely not re-accepted the current policies always sees POLICY_ACCEPTANCE_REQUIRED). A POLICY_ACCEPTANCE_REQUIRED body additionally carries details.outstanding (the policy versions still to accept) and details.firstAcceptance. The SUBSCRIPTION_REQUIRED arm is INERT unless the server-side BILLING_ENABLED flag is exactly the string true; while it is off, only the first two codes are reachable. Not retryable as sent — resolve the named condition, then resend. This operation can ALSO answer 403 with errorCode FORBIDDEN, which is a refusal by the caller’s HOUSEHOLD ROLE rather than by the tenant gate: only a household admin may perform it, and a member is refused before anything is read or written. That arm carries no details and is not cleared by resolving anything on the account — it needs an admin. It is INDEPENDENT of BILLING_ENABLED, so the reachable set here is three codes today (the two gate codes above plus FORBIDDEN) and four once billing is on. The errorCode enum on this operation’s response schema is the complete, machine-readable list.

Media typeapplication/json
object
errorCode
required

Which condition refused the request. The first three are the tenant gate, evaluated in that order (CCR-1), and SUBSCRIPTION_REQUIRED is unreachable while BILLING_ENABLED is not exactly true. FORBIDDEN is a refusal by the target resource itself (or, on POST /invites, by the caller’s household role) and carries no details.

string
Allowed values: EMAIL_NOT_VERIFIED POLICY_ACCEPTANCE_REQUIRED SUBSCRIPTION_REQUIRED FORBIDDEN
message
required
string
details
object
outstanding
required

Active policy versions the user has not yet accepted.

Array<object>
object
id
required
string format: uuid
policyType
required
string
version
required
integer
title
required
string
firstAcceptance
required

True when the user has accepted no policy before — the client renders the new-signup screen rather than the re-acceptance one.

boolean
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "EMAIL_NOT_VERIFIED"
}

The household state refuses the invite: USER_ALREADY_EXISTS (already an active member), VALIDATION_ERROR (an invite is already pending for this address), or HOUSEHOLD_FULL (the household has no free seat). Nothing was written and no email was sent. A replay of a used key answers 200, never this status.

Media typeapplication/json
object
errorCode
required

Which household-state conflict refused the invite. Nothing was written and no email was sent.

string
Allowed values: USER_ALREADY_EXISTS VALIDATION_ERROR HOUSEHOLD_FULL
message
required
string
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "USER_ALREADY_EXISTS"
}

The household has sent the most invites allowed for the rolling day. Retry after details.retryAfterSeconds.

Media typeapplication/json
object
errorCode
required

The household has sent the most invites allowed in the rolling 24-hour window.

string
Allowed values: INVITE_SEND_THROTTLED
message
required
string
details
required
object
retryAfterSeconds
required

Seconds until the oldest send in the window ages out and another invite is admitted.

integer
>= 1
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "INVITE_SEND_THROTTLED"
}

Internal error (no internal detail leaked)

Media typeapplication/json
object
errorCode
required

Machine-readable error code (AUTH_ERROR_CODES).

string
Allowed values: INVALID_EMAIL_FORMAT USER_ALREADY_EXISTS INVALID_INVITE_CODE WRONG_PASSWORD USER_NOT_FOUND AUTHENTICATION_FAILED EXPIRED_CODE INVALID_CODE INVALID_PASSWORD VALIDATION_ERROR SIGNUP_FAILED DATABASE_ERROR SERVICE_UNAVAILABLE PLAID_ERROR IDEMPOTENCY_CONFLICT ACCOUNT_ALREADY_LINKED NOT_FOUND
message
required
string
fieldErrors

Optional field-level validation errors, keyed by field name.

object
key
additional properties
Array<string>
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "INVALID_EMAIL_FORMAT"
}

Service temporarily unavailable — errorCode is SERVICE_UNAVAILABLE. A TRANSIENT, RETRYABLE condition rather than a defect in the request: a query that exceeded its time budget, a lost or refused database connection, a saturated connection pool, or an outage at an upstream provider the request depends on (the authentication provider, or the bank data provider on a bank-connection operation). The identical request may succeed on retry — back off briefly and, on a write, resend the same X-Idempotency-Key where the operation accepts one.

Media typeapplication/json
object
errorCode
required

Machine-readable error code (AUTH_ERROR_CODES).

string
Allowed values: INVALID_EMAIL_FORMAT USER_ALREADY_EXISTS INVALID_INVITE_CODE WRONG_PASSWORD USER_NOT_FOUND AUTHENTICATION_FAILED EXPIRED_CODE INVALID_CODE INVALID_PASSWORD VALIDATION_ERROR SIGNUP_FAILED DATABASE_ERROR SERVICE_UNAVAILABLE PLAID_ERROR IDEMPOTENCY_CONFLICT ACCOUNT_ALREADY_LINKED NOT_FOUND
message
required
string
fieldErrors

Optional field-level validation errors, keyed by field name.

object
key
additional properties
Array<string>
requestId
required
string
timestamp
required
string format: date-time
Example
{
"errorCode": "INVALID_EMAIL_FORMAT"
}