{"openapi":"3.1.0","info":{"title":"Signals External API","version":"1.0.0","description":"External API for placing Signals orders programmatically with `sgnl_live_...` bearer keys. JSON in, JSON out, paid from wallet balance.\n\nThis spec intentionally covers `/api/external/**` only. Storefront checkout, order-view-token links, mailbox recovery, and credential download routes are customer-account/browser APIs with separate session or signed-link auth.\n\nMint a key at [/my-account/api-keys](/my-account/api-keys), top up at [/my-account/wallet](/my-account/wallet), then quote → place → poll."},"servers":[{"url":"https://app.signals.sh","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Catalog","description":"Available services and dynamic managed-account inventory."},{"name":"Orders","description":"Quote, place, and inspect orders. Mutating endpoints allow 30 requests/minute; reads allow 60. `POST /orders` requires an `idempotencyKey` (≥ 8 chars); replays with the same key return the original `orderIds`. Every response includes `X-RateLimit-*` headers; 429 responses include `Retry-After`."},{"name":"Wallet","description":"Read-only wallet balance and history."}],"paths":{"/api/external/catalog":{"get":{"tags":["Catalog"],"summary":"List service SKUs","description":"Static service catalog with quantity bounds. Cache aggressively.","operationId":"listCatalog","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogResponse"},"example":{"success":true,"services":[{"sku":"reddit_upvotes","label":"Reddit upvotes","minQuantity":25,"maxQuantity":1000}],"accountListings":{"url":"/api/external/catalog/listings","note":"Account inventory is dynamic; query listings for availability."}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/external/catalog/listings":{"get":{"tags":["Catalog"],"summary":"List managed-account inventory","description":"Returns the current managed-account inventory. Inventory turns over quickly. Re-query right before placing a managed-account order. Use `listingId` from each entry as the cart item identifier.","operationId":"listListings","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListingsResponse"},"example":{"success":true,"accounts":[{"listingId":"f4c2a1e0-7b8d-4c2f-9e10-3a4b5c6d7e8f","catalogType":"managed_account","name":"u/ex***","totalKarma":12840,"linkKarma":8200,"commentKarma":4640,"country":"United States","age":"4.2 years","ageYears":4.2,"industry":"Technology, Finance","regularPrice":89,"salePrice":69,"goodForNsfw":false,"purchaseIncludes":["Reddit account credentials","Linked email with password"]}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/external/orders/quote":{"post":{"tags":["Orders"],"summary":"Quote a cart","description":"Re-prices a cart server-side and returns the authoritative subtotal. Always quote before placing; quotes encode line-level pricing rules (extras, delivery speed, selection tier) recomputed on every request. Rate limit: 30/minute.","operationId":"quoteOrder","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteRequest"},"example":{"items":[{"checkoutMode":"reddit_upvotes","quantity":50,"targetUrl":"https://reddit.com/r/example/comments/abc/post/"}]}}}},"responses":{"200":{"description":"Quote computed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"},"example":{"success":true,"subtotal":12.5,"currency":"USD","expiresAt":"2026-04-27T12:05:00.000Z","lines":[{"subtotal":12.5,"itemMetadata":{}}]}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/external/orders":{"get":{"tags":["Orders"],"summary":"List orders","description":"Lists your orders, newest first. Use `nextCursor` to retrieve the next page. List and detail responses use the same camelCase top-level field style.","operationId":"listOrders","parameters":[{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size (1–100, default 50)."},{"in":"query","name":"status","schema":{"type":"string"},"description":"Filter to a single order status string."},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Opaque cursor returned as `nextCursor`."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderListResponse"},"example":{"success":true,"orders":[{"id":48201,"status":"processing","orderType":"reddit_upvotes","subtotal":12.5,"creditApplied":12.5,"total":0,"createdAt":"2026-04-27T11:58:00.000Z"}],"nextCursor":null}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Orders"],"summary":"Place an order","description":"Places an order against wallet balance. The server re-quotes, checks balance, debits the wallet, and stamps the order with `order_source: \"api\"` and your key's id. A cart with multiple items may produce multiple orders, so `orderIds` is always an array. Rate limit: 30/minute.","operationId":"placeOrder","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"},"example":{"idempotencyKey":"5b3f48d2-aa1c-4d8b-9c1e-2f8a7b3c4d5e","expectedSubtotal":12.5,"items":[{"checkoutMode":"reddit_upvotes","quantity":50,"targetUrl":"https://reddit.com/r/example/comments/abc/post/"}]}}}},"responses":{"200":{"description":"Idempotent replay. The original order was already placed under this `idempotencyKey`; returns the same `orderIds`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderPlacedResponse"}}}},"201":{"description":"Order placed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderPlacedResponse"},"example":{"success":true,"orderIds":[48201],"status":"processing","subtotal":12.5,"balanceAfter":87.5,"fulfillment":{"kind":"automated","estimatedCompletionMinutes":30}}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Insufficient wallet balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceError"},"example":{"success":false,"errorCode":"INSUFFICIENT_BALANCE","error":"Insufficient wallet balance.","balance":5.25,"required":12.5}}}},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Server-quoted subtotal differs from `expectedSubtotal`. Re-quote and retry with a fresh `idempotencyKey`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceMismatchError"},"example":{"success":false,"errorCode":"PRICE_MISMATCH","error":"Quoted subtotal does not match expectedSubtotal.","quotedSubtotal":12.5,"expectedSubtotal":11.99}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/external/orders/{orderId}":{"get":{"tags":["Orders"],"summary":"Get an order","description":"Returns one order, including line items. Returns `404` if the order belongs to another customer.","operationId":"getOrder","parameters":[{"in":"path","name":"orderId","required":true,"schema":{"type":"integer"},"description":"Order id from `orderIds[]`."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderDetailResponse"},"example":{"success":true,"id":48201,"status":"processing","orderType":"reddit_upvotes","createdAt":"2026-04-27T11:58:00.000Z","subtotal":12.5,"creditApplied":12.5,"total":0,"items":[{"id":92341,"checkout_mode":"reddit_upvotes","quantity":50,"target_url":"https://reddit.com/r/example/comments/abc/post/","subtotal":12.5}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Order not found or owned by another customer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/external/wallet":{"get":{"tags":["Wallet"],"summary":"Read wallet","description":"Returns wallet balance and the ten most recent credit transactions. Top-ups happen through the storefront; the API is read-only for wallet state.","operationId":"readWallet","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletResponse"},"example":{"success":true,"balance":87.5,"currency":"USD","recentTransactions":[{"id":"1f8e2c40-9a3b-4d1e-b2c1-7e8f9a0b1c2d","type":"order_debit","amount":-12.5,"balance_after":87.5,"created_at":"2026-04-27T11:58:00.000Z"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"sgnl_live_…","description":"Mint a key at [/my-account/api-keys](/my-account/api-keys). Each customer can have one active key at a time; revoke and recreate to rotate. Keys carry a fixed permission set: `orders:create`, `orders:read`, `catalog:read`, `wallet:read`."}},"schemas":{"ServiceCartItem":{"type":"object","required":["checkoutMode","quantity"],"properties":{"checkoutMode":{"type":"string","enum":["reddit_upvotes","reddit_downvotes","reddit_comments","youtube_comments","x_comments","linkedin_comments","threads_followers","threads_likes","blog_brand_mentions","community_brand_mentions"],"description":"Service line type. Determines which pricing rules and fulfillment path apply."},"quantity":{"type":"integer","minimum":1},"targetUrl":{"type":"string","description":"Target URL for the action (e.g. the Reddit post receiving upvotes). Required for all service modes except `blog_brand_mentions` (the backend SKU for the Blog brand mentions product). For `community_brand_mentions`, pass the brand homepage URL."},"targetLabel":{"type":"string"},"deliverySpeed":{"type":"string","description":"Optional delivery-speed key (e.g. `rush`, `slow`). Affects pricing."},"extras":{"type":"array","items":{"type":"string"},"description":"Optional pricing add-ons (e.g. `smooth_curve`, `downvote_balance`)."},"selectionKey":{"type":"string","description":"Tier or selection key when the service has multiple priced tiers."},"selectionLabel":{"type":"string","nullable":true},"additionalFields":{"type":"object","additionalProperties":true,"description":"`blog_brand_mentions` (Blog brand mentions) and `community_brand_mentions` only: pass `target_brand` plus `key_phrases` containing 3 to 5 comma- or newline-separated phrases (topics, for `community_brand_mentions`). `community_brand_mentions` also requires `quantity` to be one of `5`, `10`, `20`, or `50`. Other service modes reject additional fields."}}},"ManagedAccountCartItem":{"type":"object","required":["checkoutMode","listingId"],"properties":{"checkoutMode":{"type":"string","enum":["reddit_managed_account"]},"listingId":{"type":"string","description":"`listingId` from `GET /api/external/catalog/listings`. Reservations are short-lived; quote and place quickly."}}},"CartItem":{"oneOf":[{"$ref":"#/components/schemas/ServiceCartItem"},{"$ref":"#/components/schemas/ManagedAccountCartItem"}],"discriminator":{"propertyName":"checkoutMode","mapping":{"reddit_upvotes":"#/components/schemas/ServiceCartItem","reddit_downvotes":"#/components/schemas/ServiceCartItem","reddit_comments":"#/components/schemas/ServiceCartItem","youtube_comments":"#/components/schemas/ServiceCartItem","x_comments":"#/components/schemas/ServiceCartItem","linkedin_comments":"#/components/schemas/ServiceCartItem","threads_followers":"#/components/schemas/ServiceCartItem","threads_likes":"#/components/schemas/ServiceCartItem","brand_mentions":"#/components/schemas/ServiceCartItem","blog_brand_mentions":"#/components/schemas/ServiceCartItem","reddit_managed_account":"#/components/schemas/ManagedAccountCartItem"}}},"QuoteRequest":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":25,"items":{"$ref":"#/components/schemas/CartItem"}}}},"OrderRequest":{"allOf":[{"$ref":"#/components/schemas/QuoteRequest"},{"type":"object","required":["idempotencyKey","expectedSubtotal"],"properties":{"idempotencyKey":{"type":"string","minLength":8,"description":"Caller-generated unique string (UUIDv4 recommended). Replays return the original `orderIds`."},"expectedSubtotal":{"type":"number","minimum":0,"description":"Subtotal from the matching quote response. Must match the server-recomputed quote to the cent."}}}]},"QuoteLine":{"type":"object","properties":{"subtotal":{"type":"number"},"itemMetadata":{"type":"object","additionalProperties":true},"reservation":{"type":"object","nullable":true,"properties":{"listingType":{"type":"string","enum":["managed"]},"listingId":{"type":"string"}}}}},"QuoteResponse":{"type":"object","properties":{"success":{"type":"boolean"},"subtotal":{"type":"number"},"currency":{"type":"string","enum":["USD"]},"expiresAt":{"type":"string","format":"date-time","description":"Informational (5 min from issue). The server re-quotes inside `POST /orders`; stale quotes return 409 PRICE_MISMATCH."},"lines":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLine"}}}},"OrderPlacedResponse":{"type":"object","properties":{"success":{"type":"boolean"},"orderIds":{"type":"array","items":{"type":"integer"}},"status":{"type":"string","example":"processing"},"subtotal":{"type":"number"},"balanceAfter":{"type":"number"},"fulfillment":{"type":"object","nullable":true,"properties":{"kind":{"type":"string","example":"automated"},"estimatedCompletionMinutes":{"type":"integer"}}}}},"OrderListItem":{"type":"object","properties":{"id":{"type":"integer"},"status":{"type":"string"},"orderType":{"type":"string"},"subtotal":{"type":"number"},"creditApplied":{"type":"number"},"total":{"type":"number"},"createdAt":{"type":"string","format":"date-time"}}},"OrderListResponse":{"type":"object","properties":{"success":{"type":"boolean"},"orders":{"type":"array","items":{"$ref":"#/components/schemas/OrderListItem"}},"nextCursor":{"type":"string","nullable":true}}},"OrderDetailResponse":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"integer"},"status":{"type":"string"},"orderType":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"subtotal":{"type":"number"},"creditApplied":{"type":"number"},"total":{"type":"number"},"items":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"WalletTransaction":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"amount":{"type":"number"},"balance_after":{"type":"number"},"created_at":{"type":"string","format":"date-time"}}},"WalletResponse":{"type":"object","properties":{"success":{"type":"boolean"},"balance":{"type":"number"},"currency":{"type":"string","enum":["USD"]},"recentTransactions":{"type":"array","items":{"$ref":"#/components/schemas/WalletTransaction"}}}},"CatalogService":{"type":"object","properties":{"sku":{"type":"string"},"label":{"type":"string"},"minQuantity":{"type":"integer"},"maxQuantity":{"type":"integer","nullable":true}}},"CatalogResponse":{"type":"object","properties":{"success":{"type":"boolean"},"services":{"type":"array","items":{"$ref":"#/components/schemas/CatalogService"}},"accountListings":{"type":"object","properties":{"url":{"type":"string"},"note":{"type":"string"}}}}},"Listing":{"type":"object","properties":{"listingId":{"type":"string"},"catalogType":{"type":"string","enum":["managed_account"]},"name":{"type":"string","description":"Masked account handle."},"totalKarma":{"type":"integer"},"linkKarma":{"type":"integer"},"commentKarma":{"type":"integer"},"country":{"type":"string"},"age":{"type":"string"},"ageYears":{"type":"number"},"industry":{"type":"string"},"regularPrice":{"type":"number"},"salePrice":{"type":"number"},"goodForNsfw":{"type":"boolean"},"purchaseIncludes":{"type":"array","items":{"type":"string"}}}},"ListingsResponse":{"type":"object","properties":{"success":{"type":"boolean"},"accounts":{"type":"array","items":{"$ref":"#/components/schemas/Listing"}}}},"SimpleError":{"type":"object","properties":{"success":{"type":"boolean","example":false},"errorCode":{"type":"string"},"error":{"type":"string"}}},"InsufficientBalanceError":{"allOf":[{"$ref":"#/components/schemas/SimpleError"},{"type":"object","properties":{"balance":{"type":"number"},"required":{"type":"number"}}}]},"PriceMismatchError":{"allOf":[{"$ref":"#/components/schemas/SimpleError"},{"type":"object","properties":{"quotedSubtotal":{"type":"number"},"expectedSubtotal":{"type":"number"}}}]}},"responses":{"InvalidRequest":{"description":"Schema validation failed. The `error` field carries the first failing field message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"},"example":{"success":false,"errorCode":"INVALID_REQUEST","error":"Invalid order request"}}}},"Unauthorized":{"description":"Missing, malformed, or revoked key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"},"example":{"success":false,"errorCode":"UNAUTHORIZED","error":"Invalid API key"}}}},"Forbidden":{"description":"Key lacks the required permission.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"},"example":{"success":false,"errorCode":"FORBIDDEN","error":"Missing permission"}}}},"RateLimited":{"description":"Per-key bucket exhausted. Honor the `Retry-After` response header.","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}},"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"},"example":{"success":false,"errorCode":"RATE_LIMITED","error":"Rate limit exceeded"}}}},"InternalError":{"description":"Server-side failure. Safe to retry an order with the same `idempotencyKey`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimpleError"},"example":{"success":false,"errorCode":"INTERNAL_ERROR","error":"Could not create checkout attempt"}}}}}}}