Check a promotion (dry run)
Test a promotion before you create it. Spyne runs every check a real create would run and reports the result. Nothing is created: no promotion, and no images from links.
Send the same request you'll send to Create a promotion, plus "dryRun": true.
Before you call it
A dry run checks that every vehicle is in this rooftop's inventory and not held by another hand-picked promotion, that every image id is usable, and that every image link is public and reachable. It always answers 200, even when it finds problems; read data.valid.
A request with a structural mistake, such as no name or both vehicles and criteria, gets 400 INVALID_REQUEST straight away instead of a report.
Dry runs count toward the 30 changes a minute per rooftop.
Request body
Every field from Create a promotion, plus:
| Parameter | Type | Required | Description |
|---|---|---|---|
dryRun | boolean | Required | true. Takes priority over draft: nothing is saved. |
Sample request
curl --request POST \
--url 'https://api.spyne.ai/api/pv1/promotions' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Fall Clearance",
"vehicles": ["1HGCM82633A004352", "STK-10492"],
"assets": [{ "type": "banner", "url": "https://example.com/fall-offer.png" }],
"dryRun": true
}'import requests
url = "https://api.spyne.ai/api/pv1/promotions"
headers = {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
}
payload = {
"name": "Fall Clearance",
"vehicles": ["1HGCM82633A004352", "STK-10492"],
"assets": [{"type": "banner", "url": "https://example.com/fall-offer.png"}],
"dryRun": True,
}
response = requests.post(url, headers=headers, json=payload)
print(response.status_code, response.json())const response = await fetch("https://api.spyne.ai/api/pv1/promotions", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Fall Clearance",
vehicles: ["1HGCM82633A004352", "STK-10492"],
assets: [{ type: "banner", url: "https://example.com/fall-offer.png" }],
dryRun: true,
}),
});
console.log(response.status, await response.json());Sample response
{
"success": true,
"statusCode": 200,
"message": "Dry run passed; nothing was created",
"data": {
"valid": true,
"promotionType": "custom",
"status": "active",
"vehicles": [
{ "input": "1HGCM82633A004352", "status": "available", "vin": "1HGCM82633A004352", "matchedBy": "vin" },
{ "input": "STK-10492", "status": "available", "vin": "5NPE24AF9FH000001", "matchedBy": "stockNumber" }
],
"assets": [
{
"input": { "type": "banner", "url": "https://example.com/fall-offer.png" },
"status": "ok",
"assetId": null,
"type": "banner",
"position": "all_images",
"action": "willBeCreated"
}
],
"errors": []
}
}{
"success": true,
"statusCode": 200,
"message": "Dry run found problems; nothing was created",
"data": {
"valid": false,
"promotionType": "custom",
"status": "active",
"vehicles": [
{ "input": "NOT-A-VIN", "status": "notFound" }
],
"assets": [
{ "input": { "assetId": "1584" }, "status": "ok", "assetId": "1584", "type": "banner", "position": "all_images", "action": "existing" }
],
"errors": [
{ "code": "VEHICLE_NOT_FOUND", "message": "1 vehicle(s) could not be found in inventory" }
]
}
}Response parameters (200)
| Parameter | Type | Description |
|---|---|---|
data.valid | boolean | true when nothing would stop the create. Send the same request without dryRun. |
data.promotionType | string | custom (hand-picked vehicles) or dynamic (criteria / excludeVins). |
data.status | string | How the promotion would start: active, scheduled (future start date) or draft (draft: true). |
data.vehicles[] | array | Hand-picked only; left out for filter-based. One entry per vehicle you sent, in order. A filter-based dry run can't preview how many vehicles match. |
data.vehicles[].input | string, number or object | The value you sent. |
data.vehicles[].status | string | available, notFound (also for an entry with no usable identifier), ambiguous or inOtherPromotion. |
data.vehicles[].vin | string | For available and inOtherPromotion. |
data.vehicles[].matchedBy | string | For available: vin, stockNumber or registrationNumber. |
data.vehicles[].existingPromotionId, existingPromotionName | For inOtherPromotion: the promotion that already holds the vehicle. | |
data.vehicles[].matches | object | For ambiguous: the VIN each identifier you sent points to. |
data.assets[] | array | One entry per image you sent, in order. |
data.assets[].status | string | ok, notFound (unusable image id), invalidUrl or rejected. |
data.assets[].assetId | string or null | The image id. null when action is willBeCreated. |
data.assets[].type, position, imageIndexes | For ok: the image type and where it would go, after defaults. imageIndexes only for custom. | |
data.assets[].action | string | For ok: existing (an id you sent), reused (a link already saved) or willBeCreated (a new link, saved when you create). |
data.assets[].message | string | For invalidUrl and rejected: why. |
data.errors[] | array | { code, message } for every problem a real create would stop on. Codes are listed in Errors and FAQs. |
Errors
A dry run reports vehicle and image problems in data.errors. These still come back as errors:
| Status | Code | When it happens |
|---|---|---|
400 | INVALID_REQUEST | A structural mistake in the body. details.errors lists every problem. |
400 | TEAM_NOT_RESOLVED | The API key isn't linked to a rooftop. |
401 | UNAUTHORIZED | The API key is missing or wrong. |
429 | RATE_LIMITED | More than 30 changes this minute. Wait the seconds in Retry-After. |
502 / 503 | SERVICE_UNAVAILABLE | A Spyne service is briefly unavailable. Retry shortly. |
What happens next
When data.valid is true, send the same request without dryRun to create the promotion.
Updated 2 days ago
