Skip to documentation
AdQuestDocs

Multi-step quests

A participant finishes several steps and is paid once. What your app sends, what AdQuest decides, and how to read progress.

On this page

The picture in one minute

A multi-step quest is one quest with a checklist. The participant ticks off the steps, and AdQuest pays one reward after the last required step is verified. Think of it as a boarding pass: each stamp is a step, and the reward is the flight.

IdeaWhat it means for you
One rewardThe quest pays once. A step never pays, so you never grant anything per step.
The server decidesYour app can say "start" or "click". It can never say "done". AdQuest marks a step verified.
Each participant has an executionTheir personal run of the quest. It records which steps are verified and which steps are still open.
Steps have a versionIf the owner edits the steps, new participants get the new version. People already running keep the old one.

Before you start

  • Participant sessions working, so you have a participant token for each user. See the participant sessions guide.
  • Multi-step quests are enabled for your environment. If they are not, the API answers 404 MULTI_STEP_DISABLED and the quest is not served.
  • The quest belongs to your own platform and is active. Steps that depend on your own data (condition, custom, streak) work on private quests only.
  • Your client declares the multi_step capability. The AdQuest SDK does this for you. A raw HTTP client must send the X-AdQuest-Runtime header shown below, or the quest is hidden from it.
  • Your Event signing secret, if any step is proven by your backend. It stays on your server.

Who proves each step?

Find the step in the left column. The other columns say who proves it and what you have to build.

Step typeParticipant doesProven byYou build
eventDoes something in your productYour backendSend a signed server event
conditionReaches a target, such as 3 orders or 5000 spentYour backendRegister the activity, then send signed events
custom → sdk_eventTriggers an app eventYour backendSend a signed server event. Events from the SDK only show progress.
custom → url_callbackDoes something your system can confirmYour backendAn HTTPS endpoint that answers verified: true
custom → host_verifiedGets a flag from your appYour appGrant the capability in the host context
custom → manual_reviewSubmits proofA reviewerNothing. Staff approve or reject.
streakReturns on consecutive daysYour backendCall the callback URL once per day
video, banner, survey, visitWatches, reads, answers or visitsAdQuestCall start, click or send answers. AdQuest measures time on its own.
profile, referralSaves a profile or invites a friendAdQuestNothing extra

What happens to a participant

StageWhat happensStep status
1. StartAdQuest reserves a slot and the budget, and binds the steps version to the participant.All steps pending
2. StepsEach step moves on its own as proof arrives.pending → verified (instant proof), pending → in_review → verified (a reviewer decides), in_review → rejected → in_review (resubmit, limited attempts)
3. AdmissionEvery required step is verified. AdQuest admits the execution.steps_complete is true
4. SettlementThe reward is paid once.settlement_status: pending → success
  • A verified step stays verified. Sending proof for it again changes nothing and answers 200.
  • Sequential quests unlock a step only after every required step before it is verified. Unordered quests unlock all steps at once.
  • An optional step never blocks the reward.
  • Approval does not need the participant to be online. If a reviewer approves late, AdQuest takes a new hold under the same capacity and budget rules, and retries every minute if the quest is paused or full. Nothing is paid without a hold.

Walkthrough: place 2 orders, then verify your profile

This example uses a sequential quest with two steps: a condition step called orders (count 2 distinct orders) and a profile step called profile. Follow the numbers.

  1. Register the activity once per platform (setup, not per participant).

  2. Start the quest and read the execution. You need execution_id and steps_version from it.

  3. When your backend sees an order, send a signed event for the orders step.

  4. Read the execution again to show progress. Repeat until the step says verified.

  5. The profile step unlocks. Once it is verified, AdQuest settles the quest.

1. Register the activity (setup, with your dashboard login token)
curl -X POST https://api.adquest.batriders.ir/api/v1/platforms/PLATFORM_ID/event-definitions \
  -H "Authorization: Bearer YOUR_DASHBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"order_placed","label":"Order placed","has_entity":true,"has_amount":false}'
2. Start and read the execution (what the SDK client does)
import { createAdQuestClient } from "@adquest/sdk";

const client = createAdQuestClient({ baseUrl, apiKey, participantToken, externalUserId });
await client.startQuest(23, { requiresTimer: false });
const execution = await client.getQuestExecution(23);
// execution.execution_id, execution.steps_version, execution.steps[].status
2. The same read over HTTP
curl "https://api.adquest.batriders.ir/api/v1/platform/quests/23/execution?external_user_id=user_123" \
  -H "X-AdQuest-Platform-Key: YOUR_PUBLIC_API_KEY" \
  -H "X-AdQuest-Participant-Token: TOKEN_FOR_THIS_USER" \
  -H 'X-AdQuest-Runtime: {"runtime_version":1,"capabilities":["multi_step"]}'
3. Your backend sends a signed event for one order
import { createHmac } from "node:crypto";

// executionId and stepsVersion come from the execution you just read.
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = createHmac("sha256", eventSigningSecret)
  .update(["v3", eventId, "order_placed", "23", externalUserId, timestamp, "orders", String(stepsVersion), String(executionId), orderId, "", "", ""].join("."))
  .digest("hex");

await questRequest(config, 23, "events", {
  event_id: eventId, event_name: "order_placed", source: "server", timestamp, signature,
  step_key: "orders", entity_id: orderId,
});

The response for an accepted event says whether it counted. A duplicate delivery returns the same result and does not count twice.

4. What the execution looks like mid-way
{
  "quest_id": 23,
  "execution_id": 812,
  "steps_version": 2,
  "ordering": "sequential",
  "state": "reserved",
  "expires_at": "2026-11-09T15:30:00+00:00",
  "steps": [
    { "step_key": "orders", "format": "condition", "status": "pending", "available": true,
      "progress": { "current": 1, "target": 2, "unit": "items", "satisfied": false } },
    { "step_key": "profile", "format": "profile", "status": "pending", "available": false }
  ],
  "steps_complete": false,
  "settlement_status": "none"
}

Condition steps in detail

A condition step reads: metric of an activity, operator, target. For example, count of order_placed is at least 3.

MetricCountsEvent must carry
occurrenceWhether it happened onceNothing extra
countNumber of eventsNothing extra
count_distinctDifferent entity_id values, so one order counts once even if delivered twiceentity_id (the activity is registered with has_entity)
sumTotal of amount_minor, in one currencyamount_minor as an integer in minor units, and currency (has_amount)
host_stateA flag your app granted is currently grantedNothing. It reads the host context, no events.
  • Operators: gte, gt, eq, lte, lt. There are no expressions and no AND/OR inside one step. Use two steps for two conditions.
  • Register every activity first (the call in the walkthrough). An unregistered event_name is refused with EVENT_NOT_REGISTERED.
  • Only activity that happens after the participant starts counts. In a sequential quest, only after that step unlocks.
  • Late events are accepted for up to 7 days, in any order. An event more than 60 seconds in the future is refused.
  • Amounts are integers in minor units (cents). 5000 with currency USD is 50.00 USD.

Condition events need the v3 signature. It is the v2 signature plus four fields: entity_id, amount_minor, currency and reverses_event_id. Use an empty string for any field the event does not have, as in the walkthrough.

The v3 string, field by field
const canonical = [
  "v3", eventId, eventName, String(questId), externalUserId, timestamp,
  stepKey, String(stepsVersion), String(executionId),
  entityId, amountMinor, currency, reversesEventId, // "" when the event has none
].join(".");
// signature = lowercase hex HMAC-SHA256(canonical) with your Event signing secret
A refund: register it once, then send it as an event
curl -X POST https://api.adquest.batriders.ir/api/v1/platforms/PLATFORM_ID/event-definitions \
  -H "Authorization: Bearer YOUR_DASHBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"order_refunded","label":"Order refunded","has_entity":true,"reverses":"order_placed"}'
  • Send the refund to the same step with event_name order_refunded and reverses_event_id set to the original event_id.
  • The original event stops counting. This works even if the refund arrives before the original.
  • Before the quest is admitted, a refund can move a verified step back to pending. After admission, a refund changes nothing and the reward is not taken back.

Reading the state

Poll the execution, or read one step. Use these fields to drive your screen.

FieldUse it to
steps[].statusShow pending, in_review, verified or rejected.
steps[].availableGrey out locked steps in a sequential quest.
steps[].progressShow "2 / 3 items" for a condition step.
steps[].elapsed_secondsResume a video or timer countdown after the widget reopens. The server measures it.
steps[].reviewExplain a rejection and whether the participant may resubmit.
state and expires_atShow how long the reservation is held.
steps_completeTrue when every required step is verified.
settlement_statusnone, pending or success. Credit your own app only on success.
unsettled_reasonWhy a complete execution is not paid yet, such as a paused quest. AdQuest keeps retrying.

SDK calls you will use

GoalSDK method
Start the queststartQuest(questId)
Read all stepsgetQuestExecution(questId)
Read one stepgetQuestStep(questId, stepKey)
Start, click, answer or send proof for a stepsubmitQuestStepEvidence(questId, stepKey, input)
Finish the questA state read. It returns 409 MULTI_STEP_REQUIRED until the required steps are verified. It never verifies anything.

Errors and what to do

CodeMeaningWhat to do
MULTI_STEP_DISABLEDThe feature is off, or the client did not declare multi_stepSend the runtime header, or ask AdQuest to enable it
NOT_STARTEDThe participant has no executionStart the quest first
STEP_NOT_FOUNDstep_key is not in the participant's versionRead the execution and use its keys
STEP_OUT_OF_ORDER, STEP_LOCKEDAn earlier required step is not verified yetShow the step as locked until available is true
STEP_NOT_STARTEDA timed step was submitted before startSend action start, wait, then submit
MULTI_STEP_REQUIREDComplete was called too earlyKeep showing the steps. Do not retry in a loop.
EVENT_NOT_REGISTEREDThe activity is not registered for the platformRegister it, then publish the step
INVALID_EVENT_SIGNATUREThe HMAC does not matchCheck the exact string, the order of fields and the secret
EVENT_ID_REUSEDThat event_id already proved another step or executionUse a new event_id for each real event
EVENT_WINDOW_EXPIREDThe event is older than 7 days or in the futureCheck your clock and the event time
EVENT_NAME_MISMATCHThe event is not the step's activity or its reversalSend the name the step uses
ENTITY_ID_REQUIRED, INVALID_AMOUNT, CURRENCY_MISMATCHA required attribute is missing or wrongSend entity_id, an integer amount_minor and the registered currency
EXECUTION_NOT_STARTEDThe execution or its version is not the one in the eventRead the execution again and sign with its ids
STEP_CALLBACK_ONLYYou sent evidence to a streak stepStreaks are advanced by the daily callback only
TIMING_ADMIN_ONLYAn owner set answer, gap or attempt time on a type that does not allow itOnly custom and multi-step quests accept an owner attempt time (15 minutes to 30 days). Leave timing to AdQuest elsewhere
RESERVATION_EXPIRED, QUEST_PROCESSING, QUEST_ALREADY_COMPLETEDThe hold ended, is settling or is already paidRead the execution and show the real state

Mistakes people make

  • Signing in the browser. The signing secret belongs on your backend only.
  • Using the event id of a quest-level event for a step. Each step event needs its own event_id and step_key.
  • Signing a v2 string for a condition event. Condition steps need the v3 string, with empty fields kept as empty strings.
  • Counting progress yourself. Read it from the execution.
  • Treating complete as proof. It cannot verify a step.
  • Forgetting the runtime header on raw HTTP. The quest then looks missing.
  • Editing steps and expecting running participants to change. They keep their version; only new participants get the new steps.

Next steps

Something unclear, missing or wrong here? Tell us