API
The PromoLink API
Make releases, upload their files, send them to your lists and the DJ pool, activate SmartLinks, and read who listened and downloaded, from your own systems. Keys come with the Agency plan and are made on your Account page.
Before you start
Every request goes to https://promolink.app with a JSON body, and these hold for all of them.
- Send the key with every request. In the Authorization header. A key acts as this account and may do only what was ticked for it: Read, Write, Send.
Authorization: Bearer plk_live_… - Up to 60 requests a minute. For each key. Past that the answer is 429, with Retry-After saying how many seconds to wait.
- The plan's limits hold. What the account's plan has a month (emails, SmartLinks, releases into the DJ pool) is counted the same way as in the dashboard, and the answer past it is 402. A release is sent with up to 50 tracks.
- Files. Tracks up to 120 MB (WAV is best: DJs get WAV, AIFF or MP3 made from it); covers up to 20 MB. Ask for an upload link with the file's size, and send exactly that file to it.
- Agency only. New keys come with the Agency plan, and a key answers only while the account is on it. A key that stops is kept and works again with the plan.
A release, start to finish
Upload the files, then make the release and send it in one request.
# 1. A link for each file: its size in bytes, and the file goes to it as it is
curl -X POST https://promolink.app/api/upload/presigned-url \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"filename":"01 Night Shift.wav","contentType":"audio/wav","fileType":"track","fileSize":62914560}'
# -> { "presignedUrl": "...", "tempToken": "...", "tempFilename": "..." }
curl -X PUT "$PRESIGNED_URL" -H "Content-Type: audio/wav" --data-binary @"01 Night Shift.wav"
# The cover the same way, with "fileType":"artwork" and "contentType":"image/jpeg".
# 2. The release, sent at once to the DJ pool (or to a list: "receiverListId")
curl -X POST https://promolink.app/api/campaigns \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{
"title": "Night Shift EP",
"artistName": "Your Artist",
"senderProfileId": "<one of GET /api/sender-profiles>",
"genres": ["Techno (Peak Time / Driving)"],
"tracks": [{ "title": "Night Shift", "genre": "Techno (Peak Time / Driving)",
"tempToken": "...", "tempFilename": "..." }],
"artworkTempToken": "...", "artworkTempFilename": "...",
"audiencePlatformPool": true
}'
# -> { "success": true, "campaignId": "...", "status": "processing" }
# 3. Whether it went out: "status" becomes "active"; a refused send says why in "processing_error"
curl https://promolink.app/api/campaigns?id=$CAMPAIGN_ID -H "Authorization: Bearer $KEY"Files
Tracks and covers go straight to storage, through a link you ask for first.
- POST
/api/upload/presigned-urlWriteGives a link to upload one file to, valid for 15 minutes. Send the file to it with PUT, as its raw bytes, with the same Content-Type.
filenamestring · neededThe file's name. Anything but letters, digits, dots and hyphens becomes "_". contentTypestring · neededTracks: audio/wav (best: DJs get WAV, AIFF or MP3 made from it), audio/aiff or audio/mpeg. Covers: image/jpeg, image/png or image/webp. fileType"track" | "artwork" · neededWhat the file is. fileSizeintegerThe file's size in bytes: up to 120 MB for a track, 20 MB for a cover. Give it: the link is then signed for that size, and storage takes exactly that file. campaignIduuidA release of yours (a draft) to upload into. Without it the file goes to a temporary folder, and you pass its tempToken and tempFilename when you make the release. Answers
200 { presignedUrl, key, bucketName, tempToken?, tempFilename }. With campaignId, refer to the file as fileKey = key and fileUrl = "r2://{bucketName}/{key}"; without it, as tempToken and tempFilename.- 404 "Release not found" when campaignId is not a release of yours.
Releases
A release is made, saved as a draft, and sent. Sending runs in the background.
- POST
/api/campaignsWriteMakes a release: a draft with saveAsDraft true, or sends it at once. Sending checks everything below, then prepares the tracks and starts the send by itself.
saveAsDraftbooleantrue keeps it as a draft. Anything else sends it. titlestring · neededThe release's title. artistNamestring · needed to sendAlso each track's artist unless the track names its own. senderProfileIduuid · needed to sendThe label it is from: one of yours. tracksarray · needed to send1 to 50. Each: title (to send), mixName, artistName, genre, isrcCode, and its file: fileKey and fileUrl, or tempToken and tempFilename. genresstring[] · needed to sendAt least one, from Beatport's genres. Each track's genre is held to the same list. receiverListIduuid · needed to sendOne of your mailing lists. This, or audiencePlatformPool, or both. audiencePlatformPoolboolean · needed to sendtrue puts the release in the PromoLink DJ pool, as the plan allows (Agency: 50 releases a month). artworkR2KeystringThe cover's key from the upload link (or artworkTempToken and artworkTempFilename). releaseDate, endDatedateThe release day (within a year back or ahead) and the promo's last day (at most two months after the later of today and the release day). description, catalogueNumberstringShown with the release. feedbackRequiredbooleanDJs write a short review before their first download. Answers
200 { success, campaignId, status: "processing" } when sent; { success, campaignId } for a draft.- Addresses on their own (recipientEmails) are not sent to: choose a list or the pool. Put new addresses on a list first (Contacts below).
- Whether the send went out is read afterwards with GET /api/campaigns?id=: status becomes "active", and a refused send says why in processing_error.
- 409 SAME_MUSIC: the same music as a release already out is not sent twice.
- PUT
/api/campaignsWriteSaves a draft, or sends it with saveAsDraft false. Only a draft can be changed.
campaignIduuid · neededThe draft. titlestring · neededAnd every field of POST. Answers
200 { success, campaignId }- PUT replaces the whole release: a field left out is emptied, and the tracks are the ones in the body. A PUT without tracks removes them all.
- GET
/api/sender-profilesReadYour labels: the senderProfileId a release is sent from.
Answers
200 { profiles: [ { id, name, is_default, … } ] } - GET
/api/campaignsReadEvery release of the account, newest first; with ?id= one release with its tracks and who it was sent to.
Answers
200 { campaigns: [...] }, or with ?id= { campaign, emails, tracks }. A pool DJ's address is never shown. - POST
/api/campaigns/{id}/trigger-sendWrite + SendSends an active release that has not gone out yet (one whose send was refused), now or at scheduledAt.
scheduledAtISO timeA moment in the future to send at. Answers
200 { success } or { success, scheduled: true, scheduledAt }
Who gets it
The DJ pool and more of your own contacts, for a release that is out.
- GET
/api/campaigns/pool-audience-count?genres=…ReadHow many DJs of the pool a release in these genres reaches, and whether the plan has a place for it this month.
Answers
200 { count, poolSize, allowed, monthLeft, monthOf, … } - POST
/api/campaigns/{id}/send-to-poolWritePuts a release that is out into the DJ pool. It needs a cover and a genre. Asking again only reaches DJs who joined since.
previewbooleantrue counts and changes nothing. Answers
200 { success, eligible, queued }- 402 QUOTA_EXCEEDED when the plan's pool releases of the month are used.
- POST
/api/campaigns/{id}/add-recipientsWriteSends a release that is out to more of your contacts. Anyone who has it already, or left your label, is left out.
contact_idsuuid[]Your contacts. mailing_list_idsuuid[]Your lists. manual_recipients[{ name, email }]New addresses, kept as contacts when sent to. A name is needed. previewbooleantrue counts only: nothing is sent or kept. Answers
200 { success, new_recipients_queued, duplicates_skipped }
SmartLinks
A release's SmartLink with its Artist dashboard, or a SmartLink for a release outside PromoLink.
- POST
/api/campaigns/{id}/artist-toolkitWrite + SendActivates the release's SmartLink with its Artist dashboard, or saves its settings. The first time, the artist and the people you add are emailed.
artist_name, artist_emailstring · neededWho the Artist dashboard is for. teammates[{ name, email, role? }]Others who work on this release. Sent as a list, it replaces the one there. promo_assets_url, promo_assets_notesstringA folder of assets and a message for the artist. Left out on a later save, they are emptied. Answers
200 { success, smartlink, toolkit, emailed: [...] }- Real emails go out on the first activation.
- GET
/api/campaigns/{id}/artist-toolkitReadThe release's SmartLink and Artist dashboard settings.
Answers
200 { campaign, toolkit, smartlink, teammates, tracks } - DELETE
/api/campaigns/{id}/artist-toolkitWriteTurns the release's SmartLink off.
Answers
200 { success } - POST
/api/releases/externalWriteA SmartLink for a release outside PromoLink, from its Spotify link. The same release asked again gives the one made before.
spotifyUrlstring · neededA Spotify album or track link. senderProfileIduuidThe label; your default one when left out. Answers
200 { success, release: { id, smartlinkCode, smartlinkUrl } } - GET
/api/smartlinksReadYour SmartLinks that are on (limit up to 200, offset, kind, search, sort=clicks).
Answers
200 { smartlinks: [...], total, hasMore } - GET
/api/smartlinks/{code}/analytics?period=30dReadA SmartLink's visits, clicks by platform, countries and cities (period 24h, 7d, 30d, 90d or all).
Answers
200 { overview, platforms, countries, cities, timeSeries, … }
Contacts and lists
Your own contacts and mailing lists. A contact is counted on your releases only.
- GET
/api/contacts?page=1&limit=50ReadYour contacts, with their downloads and opens of your releases. With page, limit or sortColumn it answers in pages.
Answers
200 { data, total, page, limit, totalPages } - POST
/api/contactsWriteAdds one contact. It is put on no list.
name, emailstring · neededWho it is. genre, country, roles, tagsstring[]What you know of them. Answers
201 with the contact. 409 when you have that address already. - POST
/api/contacts/importWriteAdds many contacts at once. Addresses you have already are left as they are.
csvDataarray · neededRows with email and name, and genre, country, roles, tags (comma separated). Send a few thousand rows a call at most. attestationtrue · neededYou confirm a lawful basis to email these people. It is recorded. Answers
200 { imported, failed, warnings } - GET
/api/mailing-listsReadYour mailing lists, each with its contact count.
Answers
200 [ { id, name, contact_count, contacts: [ids] } ] - GET
/api/mailing-lists/{id}/contactsReadThe contacts on one of your lists.
Answers
200 [ contacts ] - POST
/api/mailing-lists/{id}/contactsWritePuts contacts of yours on a list. Ones already on it are left.
contactIdsuuid[] · neededYour contacts. Answers
200 { success, added }
Numbers
Who listened and downloaded, and what is left of the month.
- GET
/api/campaigns/{id}/analyticsReadA release's listens, downloads and feedback, track by track, its countries, its SmartLink and its place in the pool and on the chart.
Answers
200 { tracks, totalSent, listensCount, feedbackList, geoDistribution, poolStats, chart, … } - GET
/api/usageReadThe emails of this month: sent, and left.
Answers
200 { emails: { sent, remaining, … }, resetDate, … }
Errors
A refusal has an error in its body, and often a code that says which.
- 401No key, a wrong or revoked one, or an account that is not on Agency.
- 403The key may not do this ("Insufficient scopes"), or the account is suspended.
- 402The plan: UPGRADE_REQUIRED (not on the plan) or QUOTA_EXCEEDED (the month's number is used). The body says which and links /pricing.
- 404Not yours, or not there: the two look the same.
- 409SAME_MUSIC on a send; an address you have already on POST /api/contacts.
- 429Past the key's requests a minute. Wait the seconds in Retry-After.
- 400Something in the request is not right: the body says what.
