← Divination

API

Everything you can do in Divination — open questions, forecast them, settle them, read the leaderboard — you can also do over HTTP with an API token. Point a CI job, a cron script or your own dashboard at it.

Getting a token

There are two kinds, and they work the same way: the secret starts with dvn_ and is shown once, because Divination only stores its hash — if you lose it, revoke it and make another. Whichever you mint, you choose exactly what it may do (see scopes) and when it expires.

Personal tokens

Minted at Settings → API tokens, for anything acting on your behalf.

  • A personal token acts as you. It reaches exactly the groups you belong to, and has the same rights you do in each one: any member can open questions and predict, only admins can edit, settle, annul or reopen.
  • Those rights are re-checked on every request, so losing admin (or leaving a group) takes effect immediately.
  • Actions are attributed to you, and notify the group by email and Slack exactly as if you had used the app.

Group tokens

Minted by a group admin under the group's Settings, for a script or dashboard that belongs to the group rather than to one person — it keeps working when whoever set it up moves on.

  • A group token acts for its group. It reaches that one group and nothing else, and holds admin rights in it, so it can settle and edit questions as well as read them.
  • It cannot predict: a group has no forecast of its own, so predictions:writeisn't on offer. Use a personal token to forecast.
  • Anything it writes is credited to the admin who created it, and notifies the group as usual. It keeps its rights even if that admin is demoted or leaves — any admin of the group can see and revoke it.

Scopes

A token carries only the permissions ticked when it was created, and every endpoint below names the one it needs. Grant as little as the job needs: a dashboard usually wants the three :read scopes and nothing more.

ScopeAllows
groups:readGroup details, member lists and leaderboards, for the groups the token can reach.
questions:readList questions, and read one with its options and resolution.
questions:writeOpen new questions, and edit an active one's wording, close time or prediction visibility.
questions:resolveResolve, annul and reopen questions, awarding or deleting the points that go with it.
predictions:readRead forecast histories, subject to the group's anchoring delay.
predictions:writeRecord and revise forecasts. Personal tokens only — a group has no forecast of its own.

Scopes are a ceiling, not a grant: a token still can't do anything the person or group behind it couldn't. A token asked to do something outside its scopes answers 403 and names the scope it is missing.

Making a request

Send the token as a bearer credential. Bodies are JSON; so are all responses. Timestamps are ISO 8601 in UTC, and calendar dates are plain YYYY-MM-DD.

export DIVINATION_TOKEN=dvn_…

curl -s https://divination.io/api/v1/me \
  -H "Authorization: Bearer $DIVINATION_TOKEN"

That answers with what the token is, what it may do, and the groups it can reach — the quickest way to check a token works and to find the group id you need. actor is the person its writes are credited to: its owner, or the admin who created it if it belongs to a group.

{
  "token": {
    "id": "3f2a…",
    "name": "CI pipeline",
    "type": "user",
    "scopes": ["groups:read", "questions:read", "questions:write"]
  },
  "actor": { "id": "user_2a…", "name": "Ada", "email": "ada@example.com" },
  "groups": [
    {
      "id": "9c1e…",
      "name": "Team forecasts",
      "description": null,
      "role": "admin",
      "predictionHideDays": 7,
      "predictionHideFromOpen": false,
      "createdAt": "2026-01-04T09:12:44.001Z",
      "url": "https://divination.io/groups/9c1e…"
    }
  ]
}

A group token answers the same shape with "type": "group" and exactly one group.

Endpoints

All paths are relative to /api/v1.

You and your groups

EndpointWhat it doesScope
GET /meWhat the token is, its scopes, and the groups it reaches.
GET /groupsGroups the token reaches, oldest membership first.groups:read
GET /groups/{groupId}One group, including the token's role in it.groups:read
GET /groups/{groupId}/membersMembers with their roles and join dates.groups:read
GET /groups/{groupId}/leaderboardPoints from resolved questions, highest first. Add ?month=2026-09 for one UTC month.groups:read

Questions

EndpointWhat it doesScope
GET /groups/{groupId}/questionsList them, newest first. Filters: ?status=active,resolved,annulled, ?limit= (1–200, default 50) and ?offset=. The response carries hasMore.questions:read
POST /groups/{groupId}/questionsOpen a question. Any member may. Answers 201 with the created question.questions:write
GET /questions/{questionId}One question, with its options and resolution.questions:read
PATCH /questions/{questionId}Admins only. Edit title, description (null clears it) or closesAt on an active question, or set predictionsPublic: true.questions:write
POST /questions/{questionId}/resolveAdmins only. Settle it and score everyone.questions:resolve
POST /questions/{questionId}/annulAdmins only. Void it without scoring. No body.questions:resolve
POST /questions/{questionId}/unresolveAdmins only. Reopen a settled question and delete the points it awarded. The body is optional: a closesAt moves the close time too, so it takes predictions again instead of coming back closed.questions:resolve

Predictions

EndpointWhat it doesScope
GET /questions/{questionId}/predictionsThe forecast history, honouring the group's anchoring delay.predictions:read
POST /questions/{questionId}/predictionsRecord or revise your own forecast. Answers 201. Personal tokens only.predictions:write

Opening a question

The fields mirror the “Ask a question” form. Every type needs title and closesAt (at least five minutes out), and optionally takes description and predictionsPublic. What else it needs depends on type:

typeExtra fields
binary— (a plain yes/no question)
multiple_choiceoptions: 2–10 distinct labels
numericrangeMin, rangeMax — the plausible range scoring is calibrated against — and an optional unit
daterangeMinDate, rangeMaxDate (YYYY-MM-DD)
curl -s -X POST \
  "$HOST/api/v1/groups/$GROUP_ID/questions" \
  -H "Authorization: Bearer $DIVINATION_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "type": "binary",
    "title": "Will the release ship this week?",
    "description": "Counts the moment the tag is pushed.",
    "closesAt": "2026-12-31T17:00:00Z"
  }'

The 201 response is the question itself. Keep its id — you need it to predict on or settle the question:

{
  "id": "6b0d…",
  "groupId": "9c1e…",
  "type": "binary",
  "title": "Will the release ship this week?",
  "description": "Counts the moment the tag is pushed.",
  "status": "active",
  "openedAt": "2026-08-19T10:00:00.000Z",
  "closesAt": "2026-12-31T17:00:00.000Z",
  "closed": false,
  "predictionsPublic": false,
  "createdBy": { "id": "user_2a…", "name": "Ada", "email": "ada@example.com" },
  "resolvedAt": null,
  "resolution": null,
  "url": "https://divination.io/groups/9c1e…/questions/6b0d…"
}

Type-specific fields come back the same way they go in: options on a multiple-choice question, rangeMin/rangeMax/unit on a numeric one, rangeMinDate/rangeMaxDateon a date one. Fields that don't apply are left out.

Predicting

The question's type decides the fields, so you never repeat it. Posting again revises your forecast — the history is kept, and scoring is the time-average of it, so predicting early counts for more.

# binary — p is the probability of yes, 0.01–0.99
-d '{ "p": 0.72 }'

# multiple_choice — one probability per option, summing to 1.
# Key them by option label or by option id.
-d '{ "probs": { "Alice": 0.5, "Bob": 0.3, "Carol": 0.2 } }'

# numeric — your estimate, and the half-width of your central 50% interval
-d '{ "value": 1200, "spread": 150 }'

# date — same idea, with the uncertainty in days
-d '{ "date": "2026-09-01", "spreadDays": 10 }'

Reading them back gives every revision, each with the raw payload and a human-readable summary. While a question is open, other members' recent forecasts are withheld — the same anti-anchoring delay the app applies — and othersDelayedtells you when that is happening. Once the question closes or is settled the delay is dropped and the whole history comes back. Your own predictions are always returned; a group token has none of its own, so the delay applies to every member's.

Settling a question

Which field carries the outcome depends on the question's type. Multiple choice accepts an option label (matched case-insensitively) as well as an optionId, and a binary outcome accepts true/false as well as "yes"/"no".

curl -s -X POST \
  "$HOST/api/v1/questions/$QUESTION_ID/resolve" \
  -H "Authorization: Bearer $DIVINATION_TOKEN" \
  -H 'content-type: application/json' \
  -d '{ "outcome": "yes" }'          # binary

-d '{ "option": "Alice" }'           # multiple_choice
-d '{ "value": 1187 }'               # numeric
-d '{ "date": "2026-09-03" }'        # date

The response is the resolved question, with status: "resolved" and its resolution. Got the outcome wrong? Call /unresolve, then resolve it again — the points it awarded are deleted and recomputed. Settled too soon? Reopen it with a new deadline and members can forecast on it again:

curl -s -X POST \
  "$HOST/api/v1/questions/$QUESTION_ID/unresolve" \
  -H "Authorization: Bearer $DIVINATION_TOKEN" \
  -H 'content-type: application/json' \
  -d '{ "closesAt": "2026-12-31T17:00:00Z" }'

Errors

Anything but a 2xx carries the same shape. code is stable and safe to branch on; message is for humans. Rejected input adds fieldErrors, keyed by field.

{
  "error": {
    "code": "invalid_request",
    "message": "Close time must be at least 5 minutes from now",
    "fieldErrors": { "closesAt": "Close time must be at least 5 minutes from now" }
  }
}
StatuscodeWhen
400invalid_requestMalformed or invalid body, or a bad query parameter.
401unauthorizedMissing token, or one that is unknown, revoked or expired.
403forbiddenThe token is missing the scope the endpoint needs, or the person or group behind it isn't an admin of the group and the action is admin-only.
404not_foundNo such group or question — orone you can't see, which is deliberately indistinguishable.
409conflictThe question's state rules the action out, e.g. resolving one that is already resolved.

A worked example

Open a question when a release branch is cut, and settle it when the deploy finishes:

#!/usr/bin/env bash
set -euo pipefail

HOST=https://divination.io
AUTH="Authorization: Bearer $DIVINATION_TOKEN"
JSON='content-type: application/json'

# 1. Open the question, keeping its id
QUESTION_ID=$(curl -sf -X POST "$HOST/api/v1/groups/$GROUP_ID/questions" \
  -H "$AUTH" -H "$JSON" \
  -d "{
        \"type\": \"binary\",
        \"title\": \"Will $RELEASE ship without a rollback?\",
        \"closesAt\": \"$(date -u -d '+7 days' +%Y-%m-%dT%H:%M:%SZ)\"
      }" | jq -r .id)

# 2. …a week later, settle it
curl -sf -X POST "$HOST/api/v1/questions/$QUESTION_ID/resolve" \
  -H "$AUTH" -H "$JSON" \
  -d '{ "outcome": "yes" }' > /dev/null

# 3. See who is winning
curl -sf "$HOST/api/v1/groups/$GROUP_ID/leaderboard" -H "$AUTH" \
  | jq -r '.data[] | "\(.rank). \(.user.name // .user.email)  \(.total)"'

Curious how the points are worked out? How scoring works.