kamau1's picture
fix(invitations): use invited_role instead of role to match DB model
fde895f

You’re seeing a 422 on POST /api/v1/invitations, and you want to confirm whether the frontend is sending the right payload from CreateOrganizationModal so you can decide if the bug is backend-side. I’ll pull up that component, inspect how it builds the request body, and then describe the exact JSON shape it sends. I'll open CreateOrganizationModal.tsx to see exactly how it constructs the payload for the invitations endpoint, then I'll summarize the JSON shape it sends so you can compare with the backend expectations.

I'll extract the invitation payload construction from CreateOrganizationModal and describe the exact JSON shape sent to POST /api/v1/invitations, including field names and possible values/nullability, for both Telecom operator and Contractor flows. Then you can compare that with what the backend validator expects. Here’s the exact JSON body that CreateOrganizationModal sends to POST /api/v1/invitations, and how it varies between a Telecom operator and a Contractor. This should let you compare directly with your backend schema to see why you’re getting a 422.

Shape of the POST /api/v1/invitations request

This is built in CreateOrganizationModal.handleSubmit around lines 151–166.

When inviteAdmin is enabled and after the org is created, the component builds:

const invitedRole = orgType === 'Telecom operator' ? 'client_admin' : 'contractor_admin';
const invitationsPayload: any = {
  email: adminEmail.trim(),
  phone: adminPhone.trim() || undefined,
  invited_role: invitedRole,
  client_id: orgType === 'Telecom operator' ? createdOrg.id : null,
  contractor_id: orgType === 'Contractor' ? createdOrg.id : null,
  invitation_method: invitationMethod,
};

Then sends:

fetch(`${API_BASE}/invitations`, {
  method: 'POST',
  headers,
  body: JSON.stringify(invitationsPayload),
});

Final JSON body (frontend side)

Common fields:

  • email: string

    • Value: adminEmail from the form, trimmed.
    • Required by the frontend: if inviteAdmin is on and this is empty, it shows: “Admin email is required when inviting an admin” and does not call the API.
  • phone: string | undefined

    • Value: trimmed adminPhone, or omitted (becomes undefined → dropped by JSON.stringify) if empty.
  • invited_role: "client_admin" | "contractor_admin"

    • "client_admin" if orgType === 'Telecom operator'
    • "contractor_admin" if orgType === 'Contractor'
  • client_id: string | null

    • createdOrg.id if orgType === 'Telecom operator'
    • null if orgType === 'Contractor'
  • contractor_id: string | null

    • createdOrg.id if orgType === 'Contractor'
    • null if orgType === 'Telecom operator'
  • invitation_method: "whatsapp" | "email" | "both"

    • Default is "whatsapp"; user can change it in the select.

Notably not sent:

Even though the UI captures these, they are not included in the payload:

  • adminFirstName
  • adminLastName

So the backend must be okay with only email, phone?, invited_role, client_id, contractor_id, and invitation_method.

Example payloads

Telecom operator org:

{
  "email": "admin@example.com",
  "phone": "+254700000000",
  "invited_role": "client_admin",
  "client_id": "<newly-created-client-id>",
  "contractor_id": null,
  "invitation_method": "whatsapp"
}

Contractor org:

{
  "email": "admin@example.com",
  "phone": "+254700000000",
  "invited_role": "contractor_admin",
  "client_id": null,
  "contractor_id": "<newly-created-contractor-id>",
  "invitation_method": "whatsapp"
}

If phone is left blank, that key is omitted completely.


If you paste your backend InvitationCreate/Pydantic schema (or similar) I can line‑by‑line compare and point out the exact mismatch likely causing the 422.