Import outcome history
Bring in benefits you granted before AbuseZero, so protection starts with your full history on day one.
Evaluations check every claim against your workspace's claim history. Importing the benefits you granted before AbuseZero means a user who already had a trial is recognized from the first evaluation. You can send history from code, up to 1,000 outcomes per request, or upload a CSV file of up to 100,000 rows. For how history is used, see Outcomes.
Before you begin
- The benefits you're importing exist in your workspace. Each row names a benefit by its
key. - A server key with the scopes
imports:readandimports:write. - Only granted benefits are imported. Each record needs the user's email, the benefit key, the outcome
grantedand the time it was granted, with a timezone offset. That time can't be in the future.
Import from code
Use this path when your history lives in a database you can read from your server.
Send the outcomes
Send up to 1,000 outcomes per request with the SDK's importOutcomes() or POST /v1/imports. The AbuseZero CLI command is abusezero imports create, and the AbuseZero MCP server tool is abusezero_create_outcome_import. Split larger histories into batches, each with its own Idempotency-Key.
import { abusezero } from "./lib/abusezero";
const importJob = await abusezero.importOutcomes([
{
outcome: "granted",
benefit: "pro_trial",
subject: { external_id: "user_1093", email: "sam@example.com" },
occurred_at: "2025-06-14T09:30:00Z",
external_outcome_id: "trial_1093",
},
], "import-trials-batch-0001");{
"object": "import",
"id": "imp_2d4f6a8c-1b3e-4c5d-9e7f-0a1b2c3d4e5f",
"state": "queued"
}external_outcome_id is optional. Use your own record ID so a repeated import can't create the same outcome twice.
Check import status
The import runs in the background. Read it with GET /v1/imports/{id} or abusezero imports get until state is complete. Import states lists every state.
const status = await abusezero.getImport("imp_2d4f6a8c-1b3e-4c5d-9e7f-0a1b2c3d4e5f");Import a CSV file
Use this path for a one-time export from another system. The file can be up to 25 MiB and 100,000 rows, in the file format below.
| Column | Required | Value |
|---|---|---|
email | Yes | The user's email address. |
benefit_key | Yes | The benefit's key. |
outcome | Yes | granted. |
occurred_at | Yes | When the benefit was granted, in ISO 8601 with a timezone offset. |
external_user_id | No | Your user ID. |
external_outcome_id | No | Your record ID for the grant. |
email,benefit_key,outcome,occurred_at,external_user_id,external_outcome_id
sam@example.com,pro_trial,granted,2025-06-14T09:30:00Z,user_1093,trial_1093
ari@example.com,pro_trial,granted,2025-07-02T17:05:00+02:00,user_1188,trial_1188You can also upload the file in Developers → Data import.
Create the upload
Create the upload with POST /v1/imports/csv, abusezero imports create-csv in the AbuseZero CLI, or abusezero_create_csv_import on the AbuseZero MCP server.
const upload = await abusezero.createCsvImport("import-csv-2026-10-29") as {
id: string;
url: string;
upload_headers: Record<string, string>;
};The response is an import_upload with the import's id, an upload url and the upload_headers to send with the file. Retrying with the same Idempotency-Key returns the same import with a fresh url.
Upload the file
Send the file with PUT to url, with exactly the headers in upload_headers. See Upload the file for the errors an upload can return.
import { readFile } from "node:fs/promises";
const response = await fetch(upload.url, {
method: "PUT",
headers: upload.upload_headers,
body: await readFile("history.csv"),
});
if (!response.ok) throw new Error(`Upload failed with status ${response.status}`);The upload address is temporary. If it expires before the file is sent, retry the create call with the same Idempotency-Key for a fresh address.
Validate the file
Validation checks every row before anything is imported. Start it after the upload succeeds, then read the import until state is preview. The REST API call is POST /v1/imports/{id}/validate. Validating before the file arrives returns IMPORT_NOT_UPLOADED (conflict), and the import waits in uploading.
await abusezero.validateImport(upload.id, `validate:${upload.id}`);
const checked = await abusezero.getImport(upload.id);If the import ends in failed, its failure field gives a code and a message that say what to fix (failure reasons), such as csv_columns or row_limit. Fix the file and upload it to a new import.
In preview, validated is the number of rows read and rejected the number with a problem. If any row was rejected, download the error report: GET /v1/imports/{id}/errors returns a temporary url for a CSV with each rejected row number and what to fix. The report is available until 7 days after the import completes.
Confirm the import
When the preview looks right, confirm it with POST /v1/imports/{id}/confirm or abusezero imports confirm. Rejected rows are left out.
await abusezero.confirmImport(upload.id, `confirm:${upload.id}`);The import moves to queued, then processing, then complete.
To stop an import before it completes, cancel it with POST /v1/imports/{id}/cancel. Cancelling permanently deletes the uploaded file and every row not yet imported, and the import's state becomes cancelled. It can't be undone. A complete import can't be cancelled.
Verify the result
Read the import with GET /v1/imports/{id} once it's complete.
{
"object": "import",
"id": "imp_2d4f6a8c-1b3e-4c5d-9e7f-0a1b2c3d4e5f",
"state": "complete",
"total": 2,
"validated": 2,
"processed": 2,
"imported": 2,
"skipped": 0,
"rejected": 0
}importedis the number of outcomes added to your history.skippedis the number already in your history.rejectedis the number with a problem.
Then preview a claim for an imported user with a preview, as in Test changes safely. The result reflects their earlier claim.
How is this guide?
Last updated on