Quickstart
Five steps from an empty project to a fine-tuned model answering over an OpenAI-compatible endpoint. Each one is the request that actually performs it, so this page doubles as the shortest possible integration.
Every path below is scoped to a project: {owner_slug} is your account slug and
{project_slug} is the project's. Both appear in the URL of any project screen.
01 · Get a key
Create and revoke keys from the profile menu, under API keys. A key is shown once, in the response to creating it — it is stored as a digest, so no listing can return it and nobody can recover it for you. If you lose it, revoke it and make another.
Send it as a bearer token, and keep it out of version control.
export REOPENLY_API_KEY="fnf_your_api_key"
02 · Point the SDK at your project
Every project exposes an OpenAI-compatible surface. The only change to existing code is the base URL.
pip install openai
from openai import OpenAI
client = OpenAI(
base_url="https://reopenly.com/api/projects/{owner_slug}/{project_slug}/openai/v1",
api_key="fnf_your_api_key",
)
You can stop here. A project with no dataset and no fine-tune still serves the catalog's base models through that client.
03 · Add examples
Chats are the training data. Add them from your own pipeline with the same API the editor uses, so what you curate by hand and what you import end up in one place.
POST /api/v1/projects/{owner_slug}/{project_slug}/chats
Authorization: Bearer fnf_your_api_key
Content-Type: application/json
{
"name": "returns-faq-01",
"tags": ["support"],
"messages": [
{"role": "user", "text": "How do returns work?"},
{"role": "assistant", "text": "Returns are free within 30 days."}
]
}
Tags are how a fine-tune selects its data, so they are worth choosing deliberately rather than after the fact.
Listing them back gives you message_count, a durable updated_at, and a
nullable source per chat. message_count is computed by the server,
and updated_at moves when the chat, its tags or its messages are edited.
source is one of csv, api, editor, or playground; older chats created
before provenance tracking can return null. The server decides this value, so
an API-key caller cannot claim a different ingestion path.
GET /api/v1/projects/{owner_slug}/{project_slug}/chats
04 · Fine-tune
A job is created from dataset tags and a base model, and it is measured in
steps rather than epochs — the trainer runs a fixed number of training
steps, and the bounds the API enforces are published by
GET /api/v1/base-models.
POST /api/v1/projects/{owner_slug}/{project_slug}/training-jobs
Authorization: Bearer fnf_your_api_key
Content-Type: application/json
{
"dataset_tags": ["support"],
"model_name": "<catalog-model-id>",
"max_seq_length": 2048,
"num_train_epochs": 3
}
Watch it, and stop it if you want to:
GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}
POST /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/cancel
First, ReOpenly freezes the selected chats, so later edits cannot change the run. Loading the model and reading and tokenizing your data happen before training starts, and publishing the model after it ends: only the training loop is billed. A run that never reaches its first training step is charged nothing.
Cancelling and stopping are two operations. POST .../cancel applies while the
job is queued or preparing, where nothing has been learned and nothing is
charged. Once training has begun, POST .../pause is the control: the run
keeps training to its next recovery checkpoint and settles paused, where it
can be resumed or finished. Neither applies once the job is post-processing or
finalizing.
There is no separate public evaluation or deployment job state. Eligible
runs evaluate during the training loop, upload strict loss improvements as
adapter candidates, and promote the lowest-loss verified candidate during
finalization. See Training jobs for the state table,
evaluation policy, and billing boundaries.
05 · Serve it
A job that produced an artifact can be published under a stable name you choose. Create the alias first:
POST /api/v1/projects/{owner_slug}/{project_slug}/models
Authorization: Bearer fnf_your_api_key
Content-Type: application/json
{
"name": "Support bot",
"slug": "support-bot",
"training_job_id": 87
}
From then on the slug is another model value for the same client from step
02:
response = client.chat.completions.create(
model="support-bot",
messages=[{"role": "user", "content": "How do returns work?"}],
temperature=0.7,
max_tokens=1024,
stream=True,
)
for chunk in response:
print(chunk.choices[0].delta.content or "", end="")
The weights are yours. Download a promoted LoRA adapter with
GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/download?format=lora
and run it anywhere.
Where to go next
- Authentication — key handling in full.
- Projects and datasets — project lifecycle, chat CRUD, filtering, and imports.
- OpenAI-compatible inference — models, streaming, context limits.
- Errors and retries — which failures are worth retrying, and which never are.
- Training jobs — the real state and attempt tables.
- Pricing and credits — estimates, balances, statements, and checkout.