> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kralis.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Student Integration Identifiers API

> Use GraphQL to query, create, update, synchronize, replace, and remove external student identifiers.

Student integration identifiers connect a Kralis student to the identifier used by an external system. They are useful when a partner does not use Kralis student IDs, or when an external result workbook must be matched back to Kralis records.

The identifier belongs to the active school. It does not change the Kralis student's name, username, class, or academic records.

```mermaid theme={null}
flowchart LR
    A[External student record] -->|external ID and name| B[Student integration identifier]
    B -->|student relation| C[Kralis student]
    C --> D[School-scoped GraphQL queries]
    D --> E[Partner workflow]
```

## Endpoint and authorization

Send the operations to the [Kralis GraphQL API](https://api.kralis.app/graphql/). Authenticate first and include the access token as described in [Authentication](/developers/authentication). Every operation is evaluated against the authenticated user's active school.

The query requires the identifier view permission. Identifier mutations also require the corresponding add, change, or delete permission. Kralis Web currently exposes identifier management to authorized school administrators and deans; API clients should handle permission errors rather than assuming that a visible control grants access.

Use [GraphiQL](https://api.kralis.app/graphiql/) to confirm the current schema, enum values, and generated filter argument names before shipping a client.

## Supported integrations

The `integration` value is a stable machine key:

| GraphQL enum | Stored key | Use                                                 |
| ------------ | ---------- | --------------------------------------------------- |
| `ESMOE`      | `esmoe`    | Enugu State Ministry of Education workflows         |
| `CDE`        | `cde`      | Catholic Diocese of Enugu Education Board workflows |
| `KLAPP`      | `klapp`    | Klapp school data integration                       |

The GraphQL enum is uppercase even though the stored key is lowercase. Use the enum value in GraphQL variables.

## Identifier fields

```graphql theme={null}
type StudentIntegrationIdentifierType {
  id: ID!
  pk: ID!
  student: StudentType!
  integration: CoreStudentIntegrationIdentifierIntegrationChoices!
  year: YearType
  externalId: String!
  externalName: String!
  metadata: JSONString!
}
```

`externalName` is retained as supplied by the partner. It is reference data, not a replacement for the Kralis name. `metadata` is an optional JSON object for integration-specific details.

## Year behavior

`yearId` is optional in mutation input, and `year_Id` is the generated filter argument on the root connection.

* A null year creates a yearless identifier that can be reused across academic years.
* A year-specific identifier applies to one academic year.
* A student can have one identifier per integration and year scope.
* An external ID is unique within a school, integration, and year scope.
* A nested student query with `yearId` returns the selected-year identifier first and can fall back to the yearless identifier.
* The root `studentIntegrationIdentifiers` query filters the stored rows; it does not apply the nested fallback rule.

ESMOE registration numbers are currently treated as stable across academic years, so ESMOE synchronization normally creates yearless identifiers even though the Annual year is used to load the students for the workflow.

## Query all identifiers

The root connection supports Relay pagination, generated Django-filter arguments, and a search that checks external IDs, external names, usernames, and student name fields.

```graphql theme={null}
query StudentIntegrationIdentifiers(
  $integration: CoreStudentIntegrationIdentifierIntegrationChoices
  $yearId: ID
  $studentIds: [ID!]
  $search: String
  $first: Int!
  $after: String
) {
  studentIntegrationIdentifiers(
    integration: $integration
    year_Id: $yearId
    student_In: $studentIds
    search: $search
    first: $first
    after: $after
  ) {
    totalCount
    edges {
      cursor
      node {
        id
        pk
        integration
        externalId
        externalName
        year { id pk yearName }
        student {
          pk
          userPtr { username fullName }
        }
        metadata
      }
    }
    pageInfo { hasNextPage endCursor }
  }
}
```

Example variables:

```json theme={null}
{
  "integration": "ESMOE",
  "yearId": null,
  "studentIds": ["student-id-1", "student-id-2"],
  "search": "263098",
  "first": 50,
  "after": null
}
```

`student_In` accepts multiple student IDs. Use `student` when filtering for one student. The generated connection also exposes exact and text filter variants for supported fields, including `id`, `externalId`, `externalName`, `student__user_ptr__id`, and `year_Id`; confirm exact generated names in GraphiQL because Django-filter converts model lookups to GraphQL argument names.

Example response:

```json theme={null}
{
  "data": {
    "studentIntegrationIdentifiers": {
      "totalCount": 1,
      "edges": [
        {
          "cursor": "YXJyYXljb25uZWN0aW9uOjA=",
          "node": {
            "id": "U3R1ZGVudEludGVncmF0aW9uSWRlbnRpZmllclR5cGU6MQ==",
            "pk": "1",
            "integration": "ESMOE",
            "externalId": "26309843317240",
            "externalName": "Ozonwama chiamaka",
            "year": null,
            "student": {
              "pk": "student-id-1",
              "userPtr": {
                "username": "stu-123@school",
                "fullName": "Chiamaka Ozonwama"
              }
            },
            "metadata": "{\"source\":\"sync\"}"
          }
        }
      ],
      "pageInfo": { "hasNextPage": false, "endCursor": "…" }
    }
  }
}
```

Request `id` and `pk` for identifier records. Use `endCursor` for the next page and merge pages by a stable identifier; see [Errors and Pagination](/developers/errors-and-pagination).

## Query identifiers through a student

Use the nested field when loading a student together with the identifier needed by a workflow. The `integration` and `yearId` arguments narrow the result, while the resolver prefers a selected-year row and falls back to a yearless row.

```graphql theme={null}
query StudentWithEsmoeIdentifier($studentId: ID!, $yearId: ID) {
  students(userPtr_Id: $studentId, first: 1) {
    edges {
      node {
        pk
        userPtr { username fullName }
        integrationIdentifiers(
          integration: ESMOE
          yearId: $yearId
          first: 10
        ) {
          edges {
            node {
              id
              pk
              externalId
              externalName
              year { pk yearName }
              metadata
            }
          }
        }
      }
    }
  }
}
```

For a student account ID, `userPtr_Id` is the student filter used by the `students` connection. The student object exposes its Node `id` and database `pk` for follow-up operations.

## Input objects

Create, update, and upsert use `StudentIntegrationIdentifierInput`:

```graphql theme={null}
input StudentIntegrationIdentifierInput {
  studentId: ID!
  integration: IntegrationKey!
  yearId: ID
  externalId: String!
  externalName: String
  metadata: GenericScalar
}
```

Replacement uses a separate input because changing the student assignment or external ID is an explicit correction:

```graphql theme={null}
input ReplaceStudentIntegrationIdentifierInput {
  identifierId: ID!
  studentId: ID!
  externalId: String!
  externalName: String
  metadata: GenericScalar
}
```

For JSON variables, send `metadata` as an object. On update and upsert, omitting `metadata` preserves the existing metadata. Supplying it replaces the stored metadata object.

## Create one identifier

Use create for a new mapping. It rejects invalid school relations, duplicate scoped mappings, unsupported integrations, and non-object metadata.

```graphql theme={null}
mutation CreateStudentIntegrationIdentifier(
  $input: StudentIntegrationIdentifierInput!
) {
  createStudentIntegrationIdentifier(input: $input) {
    identifier {
      id
      pk
      integration
      externalId
      externalName
      year { pk yearName }
      student { pk userPtr { username fullName } }
      metadata
    }
  }
}
```

```json theme={null}
{
  "input": {
    "studentId": "student-id-1",
    "integration": "ESMOE",
    "externalId": "26309843317240",
    "externalName": "Ozonwama chiamaka",
    "metadata": { "source": "manual" }
  }
}
```

## Update one identifier

Update changes the selected identifier's fields. It does not silently move a conflicting external identifier to another student; use the replacement mutation for reassignment.

```graphql theme={null}
mutation UpdateStudentIntegrationIdentifier(
  $id: ID!
  $input: StudentIntegrationIdentifierInput!
) {
  updateStudentIntegrationIdentifier(id: $id, input: $input) {
    identifier {
      id
      pk
      integration
      externalId
      externalName
      year { pk yearName }
      metadata
    }
  }
}
```

## Upsert confirmed mappings in bulk

`upsertStudentIntegrationIdentifiers` is for confirmed mappings. It creates missing rows and updates an existing row for the same student, integration, and year scope. It returns separate counts.

```graphql theme={null}
mutation UpsertStudentIntegrationIdentifiers(
  $inputs: [StudentIntegrationIdentifierInput]!
) {
  upsertStudentIntegrationIdentifiers(inputs: $inputs) {
    createdCount
    updatedCount
    identifiers {
      id
      pk
      student { pk }
      integration
      externalId
      externalName
      year { pk yearName }
      metadata
    }
  }
}
```

Upsert rejects duplicate students or external IDs in one batch, and rejects attempts to change an existing student's external ID or assign an already-used external ID to another student. Those cases require explicit replacement.

Example bulk response:

```json theme={null}
{
  "data": {
    "upsertStudentIntegrationIdentifiers": {
      "createdCount": 1,
      "updatedCount": 2,
      "identifiers": [
        {
          "id": "…",
          "pk": "1",
          "student": { "pk": "student-id-1" },
          "integration": "ESMOE",
          "externalId": "26309843317240",
          "externalName": "Ozonwama chiamaka",
          "year": null,
          "metadata": "{\"source\":\"sync\"}"
        }
      ]
    }
  }
}
```

## Replace identifier assignments

Use `replaceStudentIntegrationIdentifiers` after a user has confirmed a conflict. It atomically moves or changes the selected existing rows and prevents duplicate target assignments.

```mermaid theme={null}
sequenceDiagram
    participant W as Workflow
    participant K as Kralis GraphQL
    participant DB as School data
    W->>K: Query conflicting identifier and candidate student
    K->>DB: Return school-scoped records
    W->>K: replaceStudentIntegrationIdentifiers
    K->>DB: Validate and lock rows
    DB-->>K: Move/update identifier atomically
    K-->>W: replacedCount and identifiers
```

```graphql theme={null}
mutation ReplaceStudentIntegrationIdentifiers(
  $inputs: [ReplaceStudentIntegrationIdentifierInput]!
) {
  replaceStudentIntegrationIdentifiers(inputs: $inputs) {
    replacedCount
    identifiers {
      id
      pk
      student { pk userPtr { username fullName } }
      integration
      externalId
      externalName
      year { pk yearName }
      metadata
    }
  }
}
```

The replacement input contains the existing `identifierId`, the new `studentId`, and the external ID. The mutation preserves metadata unless a new metadata object is supplied. It is the safe path when an ESMOE registration number is already linked to Student A and a user confirms moving it to Student B.

## Delete identifiers

Delete one row with `deleteStudentIntegrationIdentifier`:

```graphql theme={null}
mutation DeleteStudentIntegrationIdentifier($id: ID!) {
  deleteStudentIntegrationIdentifier(id: $id) {
    deleted
  }
}
```

Delete selected rows, optionally limited to one integration and year, with `bulkDeleteStudentIntegrationIdentifiers`:

```graphql theme={null}
mutation BulkDeleteStudentIntegrationIdentifiers(
  $integration: IntegrationKey!
  $yearId: ID
  $ids: [ID!]
) {
  bulkDeleteStudentIntegrationIdentifiers(
    integration: $integration
    yearId: $yearId
    ids: $ids
  ) {
    deletedCount
  }
}
```

When `ids` is supplied, only those school-scoped rows are deleted. Without `ids`, the mutation deletes all rows in the supplied integration and optional year scope, so clients should require an explicit confirmation before calling it.

## Recommended client flow

```mermaid theme={null}
flowchart TD
    A[Load active-school students] --> B[Load identifiers for integration]
    B --> C{Identifier exists?}
    C -->|Yes| D[Use external ID directly]
    C -->|No| E[Match or ask user to select student]
    E --> F{Existing external assignment conflict?}
    F -->|No| G[Upsert confirmed mappings]
    F -->|Yes| H[Show conflict and require confirmation]
    H --> I[Replace explicitly confirmed rows]
    D --> J[Run partner workflow]
    G --> J
    I --> J
```

1. Load only the active school's students and paginate both student and identifier connections.
2. Prefer an existing identifier over name matching.
3. Treat suggested or fuzzy matches as reviewable, not automatically confirmed unless the workflow's confidence rules allow it.
4. Use upsert for new or unchanged confirmed mappings.
5. Use replacement for an existing external ID that must move to another Kralis student.
6. Refetch after mutations so the UI reflects the authoritative school-scoped state.

For the operator-facing management workflow, see [Student integration identifiers](https://docs.kralis.app/integrations/student-identifiers). For ESMOE-specific matching and workbook behavior, see the [ESMOE integration guide](/integrations/esmoe).
