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:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 5 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
| Polling | Callbacks | |
|---|---|---|
| Setup complexity | Low | Medium |
| Requires inbound endpoint | No | Yes |
| Real-time notification | No (5s lag) | Yes |
| Works behind firewalls | Yes | No |
| Best for | Scripts, prototypes | Production integrations |