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.

POSThttps://api.spyne.ai/api/pv1/promotions
AuthBearer tokenRate limit30 changes / min per rooftopCreatesNothing

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:

ParameterTypeRequiredDescription
dryRunbooleanRequiredtrue. 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)

ParameterTypeDescription
data.validbooleantrue when nothing would stop the create. Send the same request without dryRun.
data.promotionTypestringcustom (hand-picked vehicles) or dynamic (criteria / excludeVins).
data.statusstringHow the promotion would start: active, scheduled (future start date) or draft (draft: true).
data.vehicles[]arrayHand-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[].inputstring, number or objectThe value you sent.
data.vehicles[].statusstringavailable, notFound (also for an entry with no usable identifier), ambiguous or inOtherPromotion.
data.vehicles[].vinstringFor available and inOtherPromotion.
data.vehicles[].matchedBystringFor available: vin, stockNumber or registrationNumber.
data.vehicles[].existingPromotionId, existingPromotionNameFor inOtherPromotion: the promotion that already holds the vehicle.
data.vehicles[].matchesobjectFor ambiguous: the VIN each identifier you sent points to.
data.assets[]arrayOne entry per image you sent, in order.
data.assets[].statusstringok, notFound (unusable image id), invalidUrl or rejected.
data.assets[].assetIdstring or nullThe image id. null when action is willBeCreated.
data.assets[].type, position, imageIndexesFor ok: the image type and where it would go, after defaults. imageIndexes only for custom.
data.assets[].actionstringFor ok: existing (an id you sent), reused (a link already saved) or willBeCreated (a new link, saved when you create).
data.assets[].messagestringFor 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:

StatusCodeWhen it happens
400INVALID_REQUESTA structural mistake in the body. details.errors lists every problem.
400TEAM_NOT_RESOLVEDThe API key isn't linked to a rooftop.
401UNAUTHORIZEDThe API key is missing or wrong.
429RATE_LIMITEDMore than 30 changes this minute. Wait the seconds in Retry-After.
502 / 503SERVICE_UNAVAILABLEA 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.


Did this page help you?