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.

Releases

A release is made, saved as a draft, and sent. Sending runs in the background.

  • POST/api/campaignsWrite

    Makes 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/campaignsWrite

    Saves 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-profilesRead

    Your labels: the senderProfileId a release is sent from.

    Answers 200 { profiles: [ { id, name, is_default, … } ] }

  • GET/api/campaignsRead

    Every 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 + Send

    Sends 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=…Read

    How 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-poolWrite

    Puts 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-recipientsWrite

    Sends 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 }

Contacts and lists

Your own contacts and mailing lists. A contact is counted on your releases only.

  • GET/api/contacts?page=1&limit=50Read

    Your 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/contactsWrite

    Adds 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/importWrite

    Adds 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-listsRead

    Your mailing lists, each with its contact count.

    Answers 200 [ { id, name, contact_count, contacts: [ids] } ]

  • GET/api/mailing-lists/{id}/contactsRead

    The contacts on one of your lists.

    Answers 200 [ contacts ]

  • POST/api/mailing-lists/{id}/contactsWrite

    Puts 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}/analyticsRead

    A 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/usageRead

    The 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.