Recipes
Complete workflows, each one runnable as written. Set PAYCRAFT_KEY first.
Gate a release on payment readiness
The question a release pipeline should ask before shipping a build that can take money.
#!/usr/bin/env bash
set -euo pipefail
API=https://api.paycraft.mobilebytesensei.com/v1
MODE="${1:-live}" # live | test
readiness=$(curl -sf -H "Authorization: Bearer $PAYCRAFT_KEY" "$API/readiness")
not_ready=$(jq -r --arg m "$MODE" \
'.providers[] | select(.[$m + "_ready"] == false) | .provider' <<<"$readiness")
if [ -n "$not_ready" ]; then
echo "::error::Not ready for $MODE payments: $(tr '\n' ' ' <<<"$not_ready")"
# Print what a human must do, rather than just failing.
jq -r --arg m "$MODE" '
.providers[]
| select(.[$m + "_ready"] == false)
| "\n\(.provider) — \(.[$m + "_detail"])"
+ (if .manual_steps then "\n " + (.manual_steps | to_entries
| map("\(.key+1). \(.value)") | join("\n ")) else "" end)
' <<<"$readiness"
exit 1
fi
echo "All providers ready for $MODE."
Scope needed: readiness:read.
Gate on test for internal builds and live for store releases. A store provider will report
test_ready: false until someone makes a sandbox purchase — that is expected, and the script prints
the steps rather than leaving the failure unexplained.
Sync products safely in CI
#!/usr/bin/env bash
set -euo pipefail
API=https://api.paycraft.mobilebytesensei.com/v1
AUTH=(-H "Authorization: Bearer $PAYCRAFT_KEY")
report=$(curl -sf "${AUTH[@]}" "$API/sync")
count=$(jq .confirm_count <<<"$report")
if [ "$count" -eq 0 ]; then
echo "Nothing to sync."; exit 0
fi
echo "$count finding(s):"
jq -r '.findings[] | " · \(.kind) → \(.subject)"' <<<"$report"
result=$(curl -sf -X POST "${AUTH[@]}" -H 'content-type: application/json' \
-d "{\"confirm_count\": $count}" "$API/sync")
jq -r '"synced: \(.synced)"' <<<"$result"
# A 200 does NOT mean every provider succeeded.
if [ "$(jq '.failed | length' <<<"$result")" -gt 0 ]; then
jq -r '.failed[] | " FAILED \(.subject) — \(.detail)"' <<<"$result"
run_id=$(jq -r .run_id <<<"$result")
echo "Per-provider detail:"
curl -sf "${AUTH[@]}" "$API/sync/events?run_id=$run_id&status=failed" \
| jq -r '.data[] | " \(.provider): \(.message)"'
exit 1
fi
jq -r '.skipped[]? | " skipped \(.subject) — \(.detail)"' <<<"$result"
Scopes: products:read + products:sync.
Two details that matter. The confirm_count must come from the report you just read — a mismatch
returns 409 because the world changed between looking and acting. And failed and skipped are
inspected explicitly, because a 200 only means the drain ran.
Answer a support question
"Is this customer actually subscribed?" — and the follow-up that catches the real cases.
EMAIL="a.customer@example.com"
API=https://api.paycraft.mobilebytesensei.com/v1
AUTH=(-H "Authorization: Bearer $PAYCRAFT_KEY")
# What the provider bills — filter to live, or a test row will answer a different question.
curl -sf "${AUTH[@]}" "$API/subscribers?email=$EMAIL&mode=live" \
| jq -r '.data[] | "\(.provider) \(.plan): \(.status), renews \(.current_period_end)"'
# What PayCraft grants. These disagree during grace periods and refunds —
# and this is the one the app trusts.
curl -sf "${AUTH[@]}" "$API/entitlements?app_user_id=$EMAIL" \
| jq -r '.data[] | "\(.provider) \(.canonical_state) expires \(.expires_at) grace \(.in_grace_until)"'
Scope: subscribers:read.
If the subscription says active and the entitlement says otherwise, the entitlement is right and
something upstream failed — check /webhooks?status=failed next.
Triage after a provider incident
API=https://api.paycraft.mobilebytesensei.com/v1
AUTH=(-H "Authorization: Bearer $PAYCRAFT_KEY")
# Did we drop anything?
curl -sf "${AUTH[@]}" "$API/webhooks?status=failed&limit=50" \
| jq -r '.data[] | "\(.created_at) \(.provider) \(.event_type): \(.error_message)"'
# What changed, and who changed it?
curl -sf "${AUTH[@]}" "$API/audit?limit=50" \
| jq -r '.data[] | "\(.ts) \(.actor_type) \(.action) \(.resource)"'
Scopes: webhooks:read + audit:read.
Webhook payloads are stored redacted, so this is safe to paste into a ticket. The audit trail
includes this API's own actions as actor_type: "api_key", so automated changes are as visible as
human ones.
Snapshot the paywall to catch unintended changes
curl -sf -H "Authorization: Bearer $PAYCRAFT_KEY" \
https://api.paycraft.mobilebytesensei.com/v1/paywall \
| jq -S . > paywall.snapshot.json
git diff --exit-code paywall.snapshot.json || {
echo "::error::Paywall changed — review before shipping"; exit 1; }
Scope: paywall:read. Commit the snapshot; a paywall that changes without anyone intending it then
fails a check instead of reaching customers.
Rotate a key
Keys are stored as a hash and cannot be read back, so rotation is create-then-revoke:
- Settings → Developer API → New key with the same scopes
- Update the secret wherever the old one lives
- Confirm the new key works —
GET /v1/tenantreturns itskey.scopes - Revoke the old one
Check last_used_at on the old key before revoking. If it is still being used, something you forgot
is still pointing at it.