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_KEYwithreddit:signals:read,reddit:signals:write,reddit:posts:read,reddit:posts:write,reddit:outreach:read,reddit:outreach:sendandreddit:outreach:cancel. - An owner or admin's key for job 2, which changes what the workspace watches.
- At least one watched subreddit whose
stageisready, 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_urlto connect one. curlandjq.
Reddit signals, mentions and outreach each open and close separately. Gates lists what a closed one answers.
| Job | Scope | Saves | Used by |
|---|---|---|---|
| 1 | reddit:signals:read | subreddits.json: the watched names, limit and profile.revision | 2 |
| 2 | reddit:signals:write | Nothing | None |
| 3 | reddit:signals:read | Nothing | None |
| 4 | reddit:signals:read | group.txt: a pain point group key | 6 |
| 5 | reddit:signals:read | Nothing | None |
| 6 | reddit:signals:read | leads.json: lead key values and next_cursor | 7 |
| 7 | reddit:signals:write | Nothing | None |
| 8 | reddit:posts:write | draft.json and draft-id.txt: the saved draft and its id | 9, 10, 16 (draft-id.txt), 12 and 13 (draft.json) |
| 9 | reddit:posts:read | Nothing | None |
| 10 | reddit:posts:write | draft.json: the draft with its newest version | 12, 13 |
| 11 | reddit:outreach:read | connection.txt: a connection_id that can send | 12, 13 |
| 12 | reddit:outreach:read | posting.json | You, before job 13 |
| 13 | reddit:outreach:send | submit.json and batch.json: the submission and its batch_id | 14, 15 |
| 14 | reddit:outreach:read | Nothing | None |
| 15 | reddit:outreach:cancel | Nothing | None |
| 16 | reddit:posts:write | Nothing | None |
| 17 | reddit:signals:read | body.json and tag.txt: the tracker list and its ETag | The 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
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 see | Cause | Fix |
|---|---|---|
409 reddit_signals_off, 503 reddit_mentions_disabled or 503 outreach_disabled | That product is closed. For outreach, submitting or outreach as a whole isn't available | Retrying won't help until it opens. Gates lists each one |
409 revision_conflict | The settings changed after job 1 | Run job 1 again, then job 2 |
503 request_timeout | The call ran past its deadline, and the work may still have finished | Read 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 8 | The draft may have been saved, and no route lists saved drafts | Send it again only if a second draft is acceptable |
A row with status send_unknown | S'more can't tell whether Reddit got it | Check on Reddit. Never submit that row again |
429 rate_limited | Too many calls | Wait 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.