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

# Handle Results from CKG Ingestion Webhook

> Match a CKG ingestion webhook to your upload and act on COMPLETED, FAILED, and ABORTED states

When Suki sends a CKG ingestion webhook, the payload already contains the final result. There are no `_links` to follow.

> [!NOTE]
>
> Unlike Ambient, Form filling, and standalone E/M Coding webhooks, CKG ingestion puts the outcome in `state` and includes any error details in the body. Match the `transaction_id` to the upload you started, then act on the `state`.

Read `state` first. When it is `COMPLETED`, start Patient Summary generation for the encounters in the bundle. When it is `FAILED` or `ABORTED`, use the `error` object, when present, to decide whether to upload again or fix the bundle first.

## Prerequisites

Before you handle results, make sure you have:

* **A webhook receiver:** An endpoint that receives Suki webhooks and verifies their signatures. Refer to [Webhook quickstart](https://developer.suki.ai/documentation/webhook/quickstart.md) and [Signature verification](https://developer.suki.ai/documentation/webhook/signature-verification.md).
* **Saved upload IDs:** Store the `transaction_id` returned by [Request FHIR bundle upload URL](https://developer.suki.ai/patient-summary-api-reference/ckg-data-ingestion/upload.md) for each upload, and the `correlation_id` if you sent one.

## Match the webhook to your upload

When you request an upload URL, store the returned `transaction_id` with your own record of the upload, such as the patient, the encounter, and when you uploaded. When the webhook arrives, look up that record by `transaction_id`.

`correlation_id` helps when you group several uploads under one reference of your own, such as a batch or sync run. Send it as a query parameter when you request the upload URL, up to 128 characters. If you leave it out, Suki generates one and returns it in the webhook.

> [!TIP]
>
> Suki can deliver the same webhook more than once. Make your handler safe to run twice for the same `transaction_id` and `state`, for example by skipping work you have already done. Refer to [Webhook request and response](https://developer.suki.ai/documentation/webhook/payload-and-response.md).

## Handle results step by step

Follow this sequence after your webhook endpoint receives a CKG ingestion notification:

**1. Verify the Webhook and Return HTTP 200**

Verify the webhook signature and return HTTP **200** within 30 seconds. Do not wait for Patient Summary generation or a new upload to finish before you respond. Process the notification in a background job.

**2. Match the Transaction ID**

Look up your saved upload record with `payload.transaction_id`. If the ID is unknown, log it and stop. Do not start Patient Summary generation for an unmatched upload.

**3. Read the State**

Branch on `payload.state`. CKG payloads use `state`, not `status`. A CKG payload never includes a `status` field.

**4. Act on the Outcome**

Generate summaries, retry the upload, or flag the bundle for review based on the state and, when present, the `error` object.

## Act on each state

| `state` | What happened | Matching polling status | What to do |
| :- | :- | :- | :- |
| `COMPLETED` | The FHIR data was ingested into CKG. | `COMPLETED` | Start Patient Summary generation for the affected encounters with the [Summary Generation APIs](https://developer.suki.ai/patient-summary-api-reference/summary-generation.md). |
| `FAILED` | Ingestion failed during processing. | `FAILED` | Log `error.code` and `error.message` when present. Retry only when `error.retryable` is `true`. Otherwise fix the bundle first. |
| `ABORTED` | Ingestion stopped before it finished, for example because the upload URL expired. | `ARCHIVED` | Request a new upload URL and upload the bundle again. Do not reuse the old upload URL. |

The matching polling status is what [Check ingestion status](https://developer.suki.ai/patient-summary-api-reference/ckg-data-ingestion/ingestion-status.md) returns for the same job. The webhook reports an archived upload as `ABORTED`.

> [!NOTE]
>
> The `error` object appears only when `state` is `FAILED` or `ABORTED`, and only when Suki recorded an error for the job. Do not assume it is always present. When `FAILED` has no `error` object, treat the failure as not retryable and investigate.

## Example: handle CKG ingestion states

The following examples run after your webhook endpoint has verified the signature and returned HTTP **200**. They:

1. Match `transaction_id` to a saved upload record.
2. Branch on `state`.
3. Call your own helpers to generate summaries, upload again, or flag a bundle for review.

`start_patient_summaries`, `upload_again`, and `flag_for_review` stand in for your application logic.

**Language tabs (agents):** Equivalent code samples are available in: Python, TypeScript, Go. Humans see one language at a time. Use the variant that matches the user's stack; behavior is the same across tabs.

### Code example

> [tab] Python
>
> ```python expandable theme={"theme":{"light":"one-light","dark":"material-theme-darker"}}
> def handle_ckg_ingestion(payload, uploads):
>     transaction_id = payload["transaction_id"]
>     upload = uploads.get(transaction_id)
>     if upload is None:
>         print(f"Unknown transaction_id {transaction_id}")
>         return
>
>     state = payload["state"]
>     error = payload.get("error") or {}
>
>     if state == "COMPLETED":
>         start_patient_summaries(upload)
>     elif state == "FAILED":
>         print(f"Ingestion failed: {error.get('code')} {error.get('message')}")
>         if error.get("retryable"):
>             upload_again(upload)
>         else:
>             flag_for_review(upload, error)
>     elif state == "ABORTED":
>         upload_again(upload)
>     else:
>         print(f"Unexpected CKG state: {state}")
> ```

> [tab] TypeScript
>
> ```typescript expandable theme={"theme":{"light":"one-light","dark":"material-theme-darker"}}
> type CkgError = {
>   code?: string;
>   message?: string;
>   retryable?: boolean;
> };
>
> export function handleCkgIngestion(
>   payload: any,
>   uploads: Map<string, Upload>
> ) {
>   const upload = uploads.get(payload.transaction_id);
>   if (!upload) {
>     console.warn(`Unknown transaction_id ${payload.transaction_id}`);
>     return;
>   }
>
>   const error: CkgError = payload.error ?? {};
>
>   switch (payload.state) {
>     case "COMPLETED":
>       startPatientSummaries(upload);
>       break;
>     case "FAILED":
>       console.error(`Ingestion failed: ${error.code} ${error.message}`);
>       if (error.retryable) {
>         uploadAgain(upload);
>       } else {
>         flagForReview(upload, error);
>       }
>       break;
>     case "ABORTED":
>       uploadAgain(upload);
>       break;
>     default:
>       console.warn(`Unexpected CKG state: ${payload.state}`);
>   }
> }
> ```

> [tab] Go
>
> ```go expandable theme={"theme":{"light":"one-light","dark":"material-theme-darker"}}
> package main
>
> import "fmt"
>
> type ckgError struct {
> 	Code      string `json:"code"`
> 	Message   string `json:"message"`
> 	Retryable bool   `json:"retryable"`
> }
>
> func handleCkgIngestion(payload map[string]any, uploads map[string]Upload) {
> 	transactionID, _ := payload["transaction_id"].(string)
> 	upload, ok := uploads[transactionID]
> 	if !ok {
> 		fmt.Printf("Unknown transaction_id %s\n", transactionID)
> 		return
> 	}
>
> 	state, _ := payload["state"].(string)
>
> 	var errInfo ckgError
> 	if rawError, exists := payload["error"].(map[string]any); exists {
> 		errInfo.Code, _ = rawError["code"].(string)
> 		errInfo.Message, _ = rawError["message"].(string)
> 		errInfo.Retryable, _ = rawError["retryable"].(bool)
> 	}
>
> 	switch state {
> 	case "COMPLETED":
> 		startPatientSummaries(upload)
> 	case "FAILED":
> 		fmt.Printf("Ingestion failed: %s %s\n", errInfo.Code, errInfo.Message)
> 		if errInfo.Retryable {
> 			uploadAgain(upload)
> 		} else {
> 			flagForReview(upload, errInfo)
> 		}
> 	case "ABORTED":
> 		uploadAgain(upload)
> 	default:
> 		fmt.Printf("Unexpected CKG state: %s\n", state)
> 	}
> }
> ```

## Use polling as a fallback

If a webhook does not arrive, for example because your endpoint was down longer than Suki retries, call [Check ingestion status](https://developer.suki.ai/patient-summary-api-reference/ckg-data-ingestion/ingestion-status.md) with the `transaction_id`. Status stays available for **7 days** after the upload. A polling status of `ARCHIVED` is the same outcome as a webhook `state` of `ABORTED`.

## Next steps

&#x20;**[CKG ingestion payload reference](https://developer.suki.ai/documentation/webhook/ckg-ingestion/payload.md)** - Every field in the payload and the error object.

&#x20;**[Patient Summary basic usage](https://developer.suki.ai/documentation/how-to/patient-summary/basic-usage.md)** - Generate summaries after ingestion completes.

&#x20;**[Check ingestion status](https://developer.suki.ai/patient-summary-api-reference/ckg-data-ingestion/ingestion-status.md)** - Poll an upload by transaction ID.

&#x20;**[Webhook request and response](https://developer.suki.ai/documentation/webhook/payload-and-response.md)** - The 200 response, retries, and duplicate deliveries.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.