Skip to main content

Polling vs Callbacks

After submitting a mission, you have two ways to retrieve results.

Polling

Poll GET /missions/{id} on an interval until status is "completed" or "failed".

When to use polling:

  • Simple scripts and one-off jobs
  • Environments where you can't receive inbound webhooks
  • Quick prototypes

Recommended interval: 5 seconds. Missions typically complete in 1–5 minutes depending on document length and mission type.

async function pollUntilDone(missionId, apiKey) {
const url = `https://app.drafted.li/api/external/v1/missions/${missionId}`;
const headers = { 'Authorization': `Bearer ${apiKey}` };

const MAX_ATTEMPTS = 120; // 10 minutes at 5s intervals
for (let i = 0; i < MAX_ATTEMPTS; i++) {
await new Promise(r => setTimeout(r, 5000));

const res = await fetch(url, { headers });
const data = await res.json();

if (data.mission.status === 'completed') return data.results;
if (data.mission.status === 'failed') throw new Error('Mission failed');
}

throw new Error('Timed out waiting for mission to complete');
}
# Bash polling loop
MISSION_ID="7f3a9c12-..."
while true; do
sleep 5
STATUS=$(curl -s \
-H "Authorization: Bearer dk_live_YOUR_KEY" \
"https://app.drafted.li/api/external/v1/missions/$MISSION_ID" \
| jq -r '.mission.status')
echo "Status: $STATUS"
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
break
fi
done

Callbacks (webhooks)

Pass a callbackUrl when creating the mission. Drafted will POST the results to that URL when the mission completes.

When to use callbacks:

  • Production integrations and automations
  • Workflows triggered by external events (e.g. Monday.com, Zapier)
  • When you want to avoid polling overhead

Setting a callback URL

{
"teamId": "...",
"missionType": "redline",
"document": { ... },
"callbackUrl": "https://your-system.com/webhooks/drafted"
}

Callback payload

When the mission completes, Drafted sends a POST to your callbackUrl:

{
"event": "mission.completed",
"mission": {
"id": "7f3a9c12-...",
"name": "Contract Review — Acme Corp",
"missionType": "redline",
"status": "completed",
"stage": "done",
"externalRef": "monday-item-12345"
},
"results": {
"findingsCount": 8,
"riskLevel": "high",
"summary": "Found 8 issues across 5 clauses...",
"findings": [ ... ],
"redlinedDocumentUrl": "https://storage.googleapis.com/..."
}
}

On failure, event is "mission.failed" and results is null.

Expected response

Your endpoint must return a 2xx status code to acknowledge receipt. Any non-2xx response is treated as a failure.

Retry behavior

If your endpoint returns a non-2xx response or times out, Drafted retries with exponential backoff:

AttemptDelay
1Immediate
230 seconds
35 minutes

After 3 failed attempts, the callback is abandoned. Use polling as a fallback if callbacks are critical.

Validating callbacks

Every callback includes an X-Drafted-Signature header. Validate it to confirm the request came from Drafted:

import crypto from 'crypto';

function validateDraftedCallback(body, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(body) // raw request body as a string
.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}

// In your webhook handler:
app.post('/webhooks/drafted', (req, res) => {
const sig = req.headers['x-drafted-signature'];
const rawBody = req.rawBody; // preserve raw body before JSON parsing

if (!validateDraftedCallback(rawBody, sig, process.env.DRAFTED_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}

const payload = JSON.parse(rawBody);
// handle payload...
res.sendStatus(200);
});

Your webhook secret is available in Settings → Developer alongside your API keys.

Comparison

PollingCallbacks
Setup complexityLowMedium
Requires inbound endpointNoYes
Real-time notificationNo (5s lag)Yes
Works behind firewallsYesNo
Best forScripts, prototypesProduction integrations