Quickstart
Get a free test key, launch a client's project in test mode and receive a signed webhook when one of its listings goes live.
This guide takes one client's product from a URL to a live listing in test mode, and receives that change as a signed webhook. Test mode is free: nothing reaches a directory and nothing is spent. Each step shows the request in cURL, Node and Python, and the response under it.
Before you start
- A free account. Create your agency at app.submitator.com/agency/start. Test mode needs no payment.
- A test key. In the console, open Developers, choose the TEST tab and create a key. It starts with
sbm_test_and is shown once. - An HTTPS URL that answers a POST, for step 6. Your own server works, or a request inspector for a first look.
Put the key in an environment variable, so it stays out of your code:
export SUBMITATOR_API_KEY="sbm_test_..."The Node examples run as ES modules (.mjs) on Node 20 or later. The Python examples need pip install requests.
1. Check your key
GET /ping returns the account and the key behind the request.
curl https://api.submitator.com/v1/ping \
-H "Authorization: Bearer $SUBMITATOR_API_KEY"{
"object": "ping",
"ok": true,
"livemode": false,
"account": { "name": "Northlight Studio", "state": "setup" },
"key": { "id": "key_3Rw8Kx2Lq9Vt5Nb1Zm7HcW", "permissions": "full" }
}livemode is false: every call with this key runs in test mode. A 401 means the key is missing or wrong, see api_key_invalid.
2. Create a project
Send the site's URL and a contact email; external_id holds your own reference. Test mode accepts sites on .test, so the example runs as it is.
curl https://api.submitator.com/v1/projects \
-H "Authorization: Bearer $SUBMITATOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://ledgerly.test", "contact_email": "founder@ledgerly.test", "external_id": "crm_8812"}'{
"object": "project",
"id": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
"livemode": false,
"status": "draft",
"url": "https://ledgerly.test",
"name": "Ledgerly",
"contact_email": "founder@ledgerly.test",
"logo": null,
"screenshots": [],
"autofill": { "status": "running", "name_source": "domain", "filled": [], "image_candidates": [] },
"readiness": { "ready": false, "missing": ["logo", "screenshots"] },
"external_id": "crm_8812",
"created_at": "2026-10-03T13:58:12Z",
"launched_at": null
}Keep the id: every next step uses it.
3. Add a logo and a screenshot
Import each image from a URL, or upload the file as multipart/form-data with a file field. In test mode we check the request but download and keep nothing, so an image's url is a placeholder.
curl -X PUT https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP/logo \
-H "Authorization: Bearer $SUBMITATOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://ledgerly.test/press/logo.png"}'
curl https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP/screenshots \
-H "Authorization: Bearer $SUBMITATOR_API_KEY" \
-F "file=@dashboard.png"{
"object": "project",
"id": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
"status": "draft",
"logo": { "object": "image", "id": "img_5Mx2Kq8Wt3Lr9Vb7Nz1HdQ", "status": "ready", "error": null },
"screenshots": [
{ "object": "image", "id": "img_2Kr9Lx3Wq7Mt5Vb8Nz1HcF", "status": "ready", "error": null }
],
"readiness": { "ready": true, "missing": [] }
}A project holds 1 logo and 1 to 5 screenshots. An image URL that ends in /fail.png fails on purpose, so you can see how your code handles 422 image_unreachable.
4. Preview and launch
GET /projects/{project_id}/readiness is free and shows what a launch would do now. Before your first purchase, test mode picks from the top 20 directories of the catalog, so a launch gets 20 of them at most.
curl https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP/readiness \
-H "Authorization: Bearer $SUBMITATOR_API_KEY"When ready is true, launch. It uses 1 of your 100 test launches, and "auto" takes the directories the project qualifies for.
curl https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP/launch \
-H "Authorization: Bearer $SUBMITATOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"directories": "auto"}'{
"object": "project",
"id": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
"livemode": false,
"status": "in_progress",
"launched_at": "2026-10-03T14:05:40Z",
"allowance": { "total": 100, "used": 0, "reserved": 16, "available": 84 },
"progress": {
"total": 16,
"pending": 16,
"in_progress": 0,
"action_required": 0,
"submitted": 0,
"live": 0,
"not_accepted": 0,
"cancelled": 0
}
}From here the listings move on their own and the project completes in about 15 minutes. Step 7 moves one of them right away.
5. Find a listing
Each directory gets one listing. Take the first one and keep its id.
curl "https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP/listings?limit=1" \
-H "Authorization: Bearer $SUBMITATOR_API_KEY"{
"object": "list",
"data": [
{
"object": "listing",
"id": "lst_7Hk2Qn4Rt9Vx1Bm6Cz3LdK",
"livemode": false,
"directory": { "id": "dir_2Lm8Wq3Zk7Rt1Xc5Vb9NdF", "name": "BetaList", "domain_rating": 73 },
"status": "pending",
"live_url": null
}
],
"has_more": true,
"next_cursor": "cur_8Tq2Xn5Wb1Lz7Pd3"
}6. Add a webhook endpoint
Register the URL that should receive events. Replace https://hooks.northlight.test/listings with yours: it must be a public https URL.
curl https://api.submitator.com/v1/webhook_endpoints \
-H "Authorization: Bearer $SUBMITATOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://hooks.northlight.test/listings", "events": ["listing.*", "project.*"]}'{
"object": "webhook_endpoint",
"id": "we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT",
"livemode": false,
"url": "https://hooks.northlight.test/listings",
"events": ["listing.*", "project.*"],
"enabled": true,
"secret": "whsec_9laRgOMT0C3d8Je6FbArCUY0bGJM+r2emaNUGJCH+aw=",
"created_at": "2026-10-06T12:00:04Z"
}The secret is shown in this response only. Store it as SUBMITATOR_WEBHOOK_SECRET for step 8.
7. Move the listing to live
The test helper moves a test listing 1 step along the usual path on each call: in_progress, then submitted, then live. Each move records its event and sends it to your endpoint.
for step in 1 2 3; do
curl https://api.submitator.com/v1/test_helpers/listings/lst_7Hk2Qn4Rt9Vx1Bm6Cz3LdK/advance \
-H "Authorization: Bearer $SUBMITATOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
done{
"object": "listing",
"id": "lst_7Hk2Qn4Rt9Vx1Bm6Cz3LdK",
"livemode": false,
"directory": { "id": "dir_2Lm8Wq3Zk7Rt1Xc5Vb9NdF", "name": "BetaList", "domain_rating": 73 },
"status": "live",
"live_url": "https://betalist.com/startups/ledgerly",
"live_at": "2026-10-08T14:00:00Z"
}If the simulation moved the listing first, a call can return 422 validation_failed. Pick another listing from step 5.
8. Check the webhook
Your endpoint received listing.in_progress, listing.submitted and listing.live for this listing. The last one looks like this:
POST /listings HTTP/1.1
Host: hooks.northlight.test
Content-Type: application/json
webhook-id: evt_5Rt8Wq2Lm7Xn3Kb9Vz1PdJ
webhook-timestamp: 1791469022
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{"object":"event","id":"evt_5Rt8Wq2Lm7Xn3Kb9Vz1PdJ","type":"listing.live","livemode":false,"project":"prj_4QzX1m9Lr2Vb7Nc8Tk3HwP","data":{"object":{"object":"listing","id":"lst_7Hk2Qn4Rt9Vx1Bm6Cz3LdK","status":"live"},"previous_status":"submitted"}}Check the signature before you trust the body, with the official Standard Webhooks library and your secret. A delivery that fails the check did not come from us: answer 400 and ignore it.
import { Webhook } from "standardwebhooks";
// The whsec_ secret of your endpoint, shown once when you added it.
const secret = process.env.SUBMITATOR_WEBHOOK_SECRET;
// Returns the event when the delivery is ours. Throws a
// WebhookVerificationError when the signature does not match or the
// timestamp is more than 5 minutes from your clock.
export function verifyWebhook(rawBody, headers, whsec = secret) {
return new Webhook(whsec).verify(rawBody, headers);
}Signatures has the same check in Ruby, PHP and Go, and without a library.
Check it worked
GET /webhook_endpoints/we_4Xk9Lm2Qt7Wr3Nb8Vz5HcT/deliveriesshows thelisting.livedelivery assucceeded.GET /events?project=prj_4QzX1m9Lr2Vb7Nc8Tk3HwPlistsproject.launchedand every listing event since.GET /projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwPshowsprogress.liveof 1 or more.
What can go wrong
| Code | Status | Fix |
|---|---|---|
not_ready | 422 | The project misses a logo, a screenshot or the contact email. Nothing was spent. |
endpoint_url_invalid | 422 | The webhook URL must be a public https URL, without a user name or password. |
test_mode_only | 403 | The test helper got a live key. Check that SUBMITATOR_API_KEY starts with sbm_test_. |
quota_exceeded | 422 | Before your first purchase, test mode allows 25 projects, 100 new projects a day and 3 webhook endpoints. |
Next steps
- Onboard a client's product: autofill, images and the fields directories ask for.
- Test mode: what is simulated, and the inputs that fail on purpose.
- Receive webhooks: an endpoint that handles every event once.
- Go-live checklist: from test mode to your first live launch.