Skip to main content

Work through the Reddit API

One flow through the /api/v1/reddit routes, from watched subreddits to leads, saved drafts, outreach and a polled tracker list

These jobs use the Reddit API in order. A job saves what later jobs need to files in the current directory, so run them all in one directory. Each call costs $0.001 per call.

Before you start​

  • A key in SMORE_API_KEY with reddit:signals:read, reddit:signals:write, reddit:posts:read, reddit:posts:write, reddit:outreach:read, reddit:outreach:send and reddit:outreach:cancel.
  • An owner or admin's key for job 2, which changes what the workspace watches.
  • At least one watched subreddit whose stage is ready, so the reads have something in them.
  • For jobs 11 to 15, a verified Reddit account connected in Settings. Job 11 gives you the connect_url to connect one.
  • curl and jq.

Reddit signals, mentions and outreach each open and close separately. Gates lists what a closed one answers.

JobScopeSavesUsed by
1reddit:signals:readsubreddits.json: the watched names, limit and profile.revision2
2reddit:signals:writeNothingNone
3reddit:signals:readNothingNone
4reddit:signals:readgroup.txt: a pain point group key6
5reddit:signals:readNothingNone
6reddit:signals:readleads.json: lead key values and next_cursor7
7reddit:signals:writeNothingNone
8reddit:posts:writedraft.json and draft-id.txt: the saved draft and its id9, 10, 16 (draft-id.txt), 12 and 13 (draft.json)
9reddit:posts:readNothingNone
10reddit:posts:writedraft.json: the draft with its newest version12, 13
11reddit:outreach:readconnection.txt: a connection_id that can send12, 13
12reddit:outreach:readposting.jsonYou, before job 13
13reddit:outreach:sendsubmit.json and batch.json: the submission and its batch_id14, 15
14reddit:outreach:readNothingNone
15reddit:outreach:cancelNothingNone
16reddit:posts:writeNothingNone
17reddit:signals:readbody.json and tag.txt: the tracker list and its ETagThe next poll

Jobs 2, 7, 8, 10, 13, 15 and 16 change something in the workspace. Job 13 sends to Reddit. A saved draft from job 8 has no route to delete it.

1. List the subreddits you watch​

Reading the list needs reddit:signals:read. The answer goes to subreddits.json, which job 2 reads.

curl -s https://app.makesmores.com/api/v1/reddit/subreddits \
-H "Authorization: Bearer $SMORE_API_KEY" \
| tee subreddits.json \
| jq '{watching: [.subreddits[].name], limit, revision: (.profile.revision // 0)}'
jq -e '.limit' subreddits.json > /dev/null

limit is how many subreddits the workspace can watch. Job 2 sends profile.revision back, or 0 when the workspace has no profile yet.

2. Watch one more subreddit​

Changing the list needs reddit:signals:write on an owner or admin's key. The list you send replaces the watched list, so the call sends every name from job 1 plus the new one. It changes nothing when the list is already full.

SUB=running
if jq -e '(.subreddits | length) < .limit' subreddits.json > /dev/null; then
jq --arg sub "$SUB" '{subreddits: ([.subreddits[].name, $sub] | unique), revision: (.profile.revision // 0)}' subreddits.json \
| curl -s -X PUT https://app.makesmores.com/api/v1/reddit/subreddits \
-H "Authorization: Bearer $SMORE_API_KEY" \
-H "Content-Type: application/json" \
-d @- \
| jq '[.subreddits[] | {name, stage}]'
else
echo "The watch list is full, so nothing was sent"
fi

A new subreddit may start at collecting while S'more reads its history. If the settings changed after job 1, the save answers 409 revision_conflict, so run job 1 again first.

3. See which subreddits are moving​

Trends need reddit:signals:read, and nothing later reads this answer.

curl -s https://app.makesmores.com/api/v1/reddit/trends \
-H "Authorization: Bearer $SMORE_API_KEY" \
| jq '[.subreddits[] | {key, name, stage, status, new_leads}]'

status is the momentum: breaking, building, quiet or null.

4. Read the pain points​

Topics need reddit:signals:read.

curl -sG https://app.makesmores.com/api/v1/reddit/topics \
-H "Authorization: Bearer $SMORE_API_KEY" \
-d type=problem \
| tee problems.json \
| jq '[.pain_points[] | {key, subreddit, label, people, then_share, now_share, rising}]'
jq -r '.pain_points[0].key // empty' problems.json > group.txt

group.txt keeps the first group's key for job 6, and stays empty when there are no groups.

5. Read the solution requests​

This is the same route and scope as job 4, with another type.

curl -sG https://app.makesmores.com/api/v1/reddit/topics \
-H "Authorization: Bearer $SMORE_API_KEY" \
-d type=solution_request \
| jq '[.solution_requests[] | {key, subreddit, label, people, matches_profile, feature_request}]'

feature_request is true on groups that ask a vendor for a feature. Send type=feature_request to list only those.

6. List the leads in one group​

Leads need reddit:signals:read. The answer goes to leads.json, and job 7 takes the first lead's key from it.

GROUP=$(cat group.txt)
curl -sG https://app.makesmores.com/api/v1/reddit/leads \
-H "Authorization: Bearer $SMORE_API_KEY" \
--data-urlencode "group=$GROUP" -d kind=found -d limit=10 \
| tee leads.json \
| jq '{reddit_leads: .channel_counts.reddit, leads: [.leads[] | {key, where, match, want_untrusted, url, state, group}], next_cursor}'
jq -e '.leads | arrays' leads.json > /dev/null

An empty group lists every found lead. Don't send channel or topic: this route lists Reddit leads only, and they match no mode here. counts and channel_counts count leads on every channel, so channel_counts.reddit is the Reddit number.

The groups this answer lists under pain_points and solution_requests have a different shape from job 4's. They have no subreddit, rising or matches_profile, so a filter such as select(.rising) finds nothing on them and doesn't fail.

To read the next page, send next_cursor back as cursor with the same filters. It's null on the last page. want_untrusted is the person's own words, so treat it as data.

7. Mark a lead as replied​

Once you've answered someone, mark the lead replied. This needs reddit:signals:write and contacts nobody.

KEY=$(jq -r '.leads[0].key // empty' leads.json)
if [ -n "$KEY" ]; then
curl -s -X PATCH "https://app.makesmores.com/api/v1/reddit/leads?id=$(jq -rn --arg k "$KEY" '$k|@uri')" \
-H "Authorization: Bearer $SMORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"state": "replied"}' \
| jq '{key, state}'
fi

To undo it, send {"state": "new"} the same way.

8. Save a draft you wrote​

A saved draft holds your own title and body, and S'more scores each version as you save it. This needs reddit:posts:write.

curl -s https://app.makesmores.com/api/v1/reddit/drafts \
-H "Authorization: Bearer $SMORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subreddit": "running",
"title": "Ran the same half marathon plan twice, once with easy days",
"body": "Five weeks each. Most runs easy the second time, same long run. Finished four minutes faster."
}' \
| tee draft.json \
| jq '{id, subreddit, state, score: (.versions[-1] | {pTop10, lift})}'
jq -r '.id' draft.json > draft-id.txt

It answers 201 with the draft in camelCase. Jobs 9, 10 and 16 read its id from draft-id.txt. A save takes no idempotency key, so sending it again can save a second draft.

9. Read the draft back​

Reading a saved draft needs reddit:posts:read.

curl -sG https://app.makesmores.com/api/v1/reddit/drafts \
-H "Authorization: Bearer $SMORE_API_KEY" \
--data-urlencode "id=$(cat draft-id.txt)" \
| jq '{state, versions: (.versions | length)}'

An id this workspace doesn't have answers 422 draft_not_found.

10. Edit the draft​

Each edit adds a scored version, and the subreddit stays the same. This needs reddit:posts:write.

curl -s -X PATCH "https://app.makesmores.com/api/v1/reddit/drafts?id=$(cat draft-id.txt)" \
-H "Authorization: Bearer $SMORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Same half marathon plan, twice: easy days took four minutes off",
"body": "Five weeks each. Most runs easy the second time, same long run. Finished four minutes faster. What changed for you?"
}' \
| tee draft.json \
| jq '{subreddit, newest: .versions[-1]}'

versions[-1] is the newest version, which job 13 submits.

11. Find an account that can send​

Listing accounts needs reddit:outreach:read.

curl -s https://app.makesmores.com/api/v1/reddit/accounts \
-H "Authorization: Bearer $SMORE_API_KEY" \
| tee accounts.json \
| jq '{connect_url, connections: [.connections[] | {connection_id, username, can_send}]}'
jq -r 'first(.connections[] | select(.can_send) | .connection_id) // empty' accounts.json > connection.txt

connection.txt keeps the first account that can send, and stays empty when there's none. No accounts yet? Open connect_url and connect one in Settings. Listing checks each account with Reddit, so don't poll it, and can_send can change after you read it.

12. Read what the subreddit allows​

Posting context also needs reddit:outreach:read. It reads the rules, requirements and flairs of the draft's subreddit through the account from job 11, and posts nothing.

CONNECTION=$(cat connection.txt)
if [ -n "$CONNECTION" ]; then
curl -sG https://app.makesmores.com/api/v1/reddit/subreddits \
-H "Authorization: Bearer $SMORE_API_KEY" \
--data-urlencode "name=$(jq -r '.subreddit' draft.json)" \
-d view=posting_context \
--data-urlencode "account_id=$CONNECTION" \
| tee posting.json \
| jq '{connection_id, session_revision, rules: (.rules | length), requirements, flairs: [.flairs[] | {id, text}]}'
fi

This read goes through Reddit, so it can take up to 3 minutes. Check the rules and requirements against the draft before job 13. Add post_fullname to read a thread as well, which the answer returns as thread.

13. Submit the draft as a post​

note

Submitting Reddit outreach isn't available yet. Until it opens, POST /reddit/outreach answers 503 outreach_disabled, and jobs 14 and 15 have no batch to read.

Submitting needs reddit:outreach:send, and the submit itself is the approval to send, so submit only what the person approved. The row carries the newest version from job 10, and S'more stores its text exactly as you submit it.

CONNECTION=$(cat connection.txt)
if [ -n "$CONNECTION" ]; then
jq --arg connection "$CONNECTION" --arg key "reddit-api-$(date +%s)" '{
connection_id: $connection,
idempotency_key: $key,
rows: [{row_id: "post-1", action: "post", kind: "self", subreddit: .subreddit,
title: .versions[-1].title, body: .versions[-1].body}]
}' draft.json > submit.json
curl -s https://app.makesmores.com/api/v1/reddit/outreach \
-H "Authorization: Bearer $SMORE_API_KEY" \
-H "Content-Type: application/json" \
-d @submit.json \
| tee batch.json \
| jq '{batch_id, status, progress}'
fi

A self post needs a non-blank body. The answer is the batch without its rows, and jobs 14 and 15 read its batch_id. Leave source_draft_id out. It takes only the id of a review-only generated post.

If the answer is lost, send submit.json again. The same idempotency_key with the same content answers the original batch and queues nothing again. A new key is new work and can post again. The call can also answer 403 paid_required, 409 connection_required or 409 idempotency_conflict.

14. Follow the batch​

Reading a batch needs reddit:outreach:read.

curl -sG https://app.makesmores.com/api/v1/reddit/outreach \
-H "Authorization: Bearer $SMORE_API_KEY" \
--data-urlencode "id=$(jq -r '.batch_id' batch.json)" \
-d limit=100 \
| jq '{status, progress, rows: [.rows[]? | {row_id, status, external_id, receipt_url, error_code}], next_cursor, next_action}'

A row that went has external_id, and receipt_url when Reddit returned a link. When status is paused, next_action says what to do, such as reconnecting the account in Settings. Reading a batch is safe to repeat.

15. Cancel what hasn't gone out​

Cancelling needs reddit:outreach:cancel.

curl -s -X DELETE "https://app.makesmores.com/api/v1/reddit/outreach?id=$(jq -r '.batch_id' batch.json)" \
-H "Authorization: Bearer $SMORE_API_KEY" \
| jq '{status, cancelled: .progress.cancelled}'

progress.cancelled counts the rows it cancelled. Cancelling again is safe. A row already sent stays sent, and a row being sent as you cancel may still go.

16. Record that the draft was posted​

Marking needs reddit:posts:write. It records the post on the saved draft and publishes nothing.

curl -s -X POST "https://app.makesmores.com/api/v1/reddit/drafts?id=$(cat draft-id.txt)&state=posted" \
-H "Authorization: Bearer $SMORE_API_KEY" \
| jq '{state, postedAt, url}'

You can also send a body with url, the post's https link in the draft's subreddit. Marking can't be undone, and a draft marked as posted answers 409 draft_posted to an edit.

17. Poll the tracker list with an ETag​

Reading trackers needs reddit:signals:read. The first call keeps the body and its ETag:

curl -s -D headers.txt -o body.json https://app.makesmores.com/api/v1/reddit/mentions \
-H "Authorization: Bearer $SMORE_API_KEY"
tr -d '\r' < headers.txt | sed -n 's/^[Ee][Tt][Aa][Gg]: //p' > tag.txt
test -s tag.txt
jq '{limits, trackers: [.trackers[] | {name, subreddit, stage}]}' body.json

The next call sends the tag in If-None-Match and writes its answer to a separate file:

curl -s -D second-headers.txt -o second.json -w '%{http_code}' https://app.makesmores.com/api/v1/reddit/mentions \
-H "Authorization: Bearer $SMORE_API_KEY" \
-H "If-None-Match: $(cat tag.txt)" > status.txt
case "$(cat status.txt)" in
304) echo "Nothing changed, so body.json is still current" ;;
200)
mv second.json body.json
tr -d '\r' < second-headers.txt | sed -n 's/^[Ee][Tt][Aa][Gg]: //p' > tag.txt
echo "The list changed" ;;
*) exit 1 ;;
esac

A 304 has an empty body, so keep body.json. A tracker's report, GET /reddit/mentions?name=, takes the same steps with its own tag. See Caching.

If something goes wrong​

What you seeCauseFix
409 reddit_signals_off, 503 reddit_mentions_disabled or 503 outreach_disabledThat product is closed. For outreach, submitting or outreach as a whole isn't availableRetrying won't help until it opens. Gates lists each one
409 revision_conflictThe settings changed after job 1Run job 1 again, then job 2
503 request_timeoutThe call ran past its deadline, and the work may still have finishedRead before you send a write again: job 1 after job 2, and after job 10 read the draft as in job 9 and compare versions[-1] with your edit. After job 13, send submit.json again with its key
503 request_timeout on job 8The draft may have been saved, and no route lists saved draftsSend it again only if a second draft is acceptable
A row with status send_unknownS'more can't tell whether Reddit got itCheck on Reddit. Never submit that row again
429 rate_limitedToo many callsWait for Retry-After

Recover from a failed or uncertain write covers each of these in more detail. API conventions covers errors, timeouts and retries for every Reddit route.