Snapshot import — bootstrap a character from real data

Upload a Slack export, a Gmail mbox, or a free-form description and Vilow turns it into a fully initialised character: persona, Big Five, relationship state, and seeded memories. The bot starts knowing things about itself, not from zero.

Why this exists

Manual or auto character creation gives you a blank-slate persona. Snapshot import gives you a persona grounded in real material — chat history, written prose, mailbox archives. Three things drop in pre-filled:

When to use it

If you just need a fresh fictional character, use manual or auto creation. Snapshot import shines when there's real source material the bot should already know.

How it works

1 POST your data to /v1/users/{external_id}/characters/from_snapshot. Returns 202 with a job id immediately.
2 Vilow runs the pipeline asynchronously: extract → analyze (chat-export sources) → map → apply.
3 Poll /v1/snapshot_jobs/{job_id} every few seconds. Typical completion: 15–60 s.
4 When status is completed, the response carries character_id. Chat with it through the standard /v1/chat/{external_id}/{character_id}/send endpoint — the character is fully active.

Three data sources

SourceBest forInputNotes
plain_text Free-form description, journaling, written portrait Up to 500 KB of text + an optional 8000-char description No analyzer stage — the prose is fed directly to the mapper. Fastest path.
slack_export Bringing in someone's voice from chat history A standard Slack workspace export .zip + their Slack user id (e.g. U02ABCD) Filters to messages the subject sent. Bot messages and join/leave noise are skipped. Slack-specific markup (<@U…>, link tags) is normalised.
gmail_mbox Email archives — long-form prose voice .mbox from Google Takeout (or a single .eml) + sender email Filters to outbound messages from the target address. Quoted replies, signatures, HTML and auto-replies are stripped.

Quick start (curl, plain text)

curl -X POST https://api.vilow.dev/v1/users/u_alex/characters/from_snapshot \
  -H "X-API-Key: $VILOW_API_KEY" \
  -F "subject_name=Marek Sokol" \
  -F "data_source=plain_text" \
  -F "subject_description=Marek is a 41-year-old Czech architect..."

Response (202 Accepted):

{
  "snapshot_job_id": "snap_9ad6eb14a6f04a9c...",
  "status": "pending",
  "estimated_seconds": 60
}

Poll until completion:

curl https://api.vilow.dev/v1/snapshot_jobs/snap_9ad6eb14a6f04a9c... \
  -H "X-API-Key: $VILOW_API_KEY"

# {"snapshot_job_id":"snap_…","status":"completed",
#  "character_id": 42,"extraction_confidence": 0.9,"warnings": [],
#  "error": null,"created_at":"…","completed_at":"…"}

Then chat as usual:

curl -X POST https://api.vilow.dev/v1/chat/u_alex/42/send \
  -H "X-API-Key: $VILOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message":"Hey, how is the Cascais project going?"}'

Quick start (Slack export)

curl -X POST https://api.vilow.dev/v1/users/u_alex/characters/from_snapshot \
  -H "X-API-Key: $VILOW_API_KEY" \
  -F "subject_name=Lena" \
  -F "data_source=slack_export" \
  -F "target_identifier=U02ABCD" \
  -F "file=@workspace_export.zip"

Larger uploads take longer to extract, but the API call still returns 202 immediately and you poll the job for completion.

Quick start (Gmail mbox)

curl -X POST https://api.vilow.dev/v1/users/u_alex/characters/from_snapshot \
  -H "X-API-Key: $VILOW_API_KEY" \
  -F "subject_name=Sofia" \
  -F "data_source=gmail_mbox" \
  -F "target_identifier=sofia@example.com" \
  -F "file=@Mail.mbox"

Mbox files from Google Takeout often run several gigabytes. We process them in-place — no need to pre-filter on your side. Only messages with From: sofia@example.com are kept and analysed.

What lands on the character

After completion, the character row has these fields populated from the mapping:

FieldSource
name, gender, persona, backstoryMapper output
big_five (0..1 × 5)Calibrated from communication patterns / description
custom_traitsCommunication style, quirks, decision tendencies in prose
default_languageDetected from source material
trust, friendship, relationship_stageSet authoritatively from relationship_to_subject when provided. Otherwise inferred from cues in the source — defaults to strangers when no signal of prior history.
user_relationship_labelEither the value of relationship_to_subject from the form, or a phrase the mapper inferred from the source. Shown to the bot at chat time as "the person you're talking to is your X" so the social bond feels real on turn one.
signature_phrases (0–14 strings)Verbatim short phrases the source shows the subject actually uses ("Right.", "Mm.", "слушай", "короче лан"). The chat system prompt injects them as concrete style anchors — the bot's voice becomes recognisable without us writing custom rules. Mapper extracts; chat-export sources usually yield 8–12, plain-text imports yield fewer unless the description quotes the subject directly.
emotions (6-dim wheel, 0..10 each)Seeded if the source describes a current emotional state ("currently tired", "thrilled about X")
Seed Memory rows (8–14)Concrete specifics: tools, places, hobbies-with-objects, ongoing concerns, family details

The job result also returns:

Signal floor

The job rejects with 422 if the input is too thin to ground a character: fewer than 30 messages and no documents and a description shorter than 200 characters. Drop more signal in or use a richer source.

Endpoint reference

POST /v1/users/{external_id}/characters/from_snapshot

Multipart/form-data. Accepts an upload up to 200 MB, text up to 500 KB.

FieldRequiredTypeNotes
subject_nameyesstring ≤ 120Display name; stored on the character.
data_sourceyesplain_text · slack_export · gmail_mbox
subject_descriptionplain_text only (recommended)string ≤ 8000Importer's prose about the subject. For chat-export sources, optional context.
text_contentplain_text onlystring ≤ 500 KBLong-form text — pasted journal, transcript, etc. Combined with subject_description.
target_identifierslack_export, gmail_mboxstring ≤ 200Slack user id or sender email. Filter cue.
fileslack_export, gmail_mboxbinary ≤ 200 MB.zip for Slack, .mbox/.eml for Gmail.
relationship_to_subjectnostring ≤ 120Free-form social role of the user toward the subject — "my husband", "my late grandmother", "old friend Sofia", "hairdresser". When provided, seeds trust, friendship and relationship_stage authoritatively, and the bot is told to address the user in that role from turn one. When omitted, the mapper looks for cues in the description / messages — falls back to strangers when nothing speaks for a prior bond.

Returns 202 Accepted:

{ "snapshot_job_id": "snap_…", "status": "pending", "estimated_seconds": 60 }

GET /v1/snapshot_jobs/{snapshot_job_id}

Poll for status. Returns:

{
  "snapshot_job_id":"snap_…",
  "status":"pending|running|completed|failed",
  "character_id": 42,
  "extraction_confidence": 0.9,
  "warnings": [],
  "error": null,
  "created_at": "...",
  "completed_at": "..."
}

Status codes

CodeMeaning
202Job accepted, processing.
400Missing required field or data_source/file mismatch.
402Snapshot quota exhausted, card declined / authentication required for overage charge, or no payment method on file. Detail body has a specific code: snapshot_lifetime_cap_reached, snapshot_quota_exhausted, card_declined, authentication_required, no_payment_method, subscription_inactive.
404External user not found.
413Upload exceeds 200 MB or text exceeds 500 KB.
422Input doesn't meet the signal floor — see signal floor.

Quotas and pricing

Snapshot imports are counted independently from chat messages — they consume a separate counter on the tenant.

PlanIncludedOverage
Free1 lifetimeHard cap — upgrade to import more.
Hobby3 / period€7 per import, charged immediately to your saved card.
Pro20 / period€7 per import, charged immediately to your saved card.
EnterpriseUnlimited

Overage charges go through Stripe directly — no top-up required. We charge the same payment method that pays your subscription, off-session, before the pipeline runs. If the card is declined the API returns 402 and no character is created.

A failed job is not counted against quota — the counter is refunded automatically. If the failed job was an overage charge, the €7 is refunded to the same card via stripe.Refund.create immediately. The SnapshotJob row keeps the stripe_payment_intent_id and refunded_at for traceability.

Privacy and retention

Best practices

What's not in scope (yet)