Skip to main content
Use soundlink/api-emulator to exercise wallet campaign flows on your machine. It is a stateful stand-in for a subset of the Public API. It does not call Meta and it does not spend. The import is not re-exported from soundlink. Requires soundlink 2.10.0 or later. For the typed client against production, see Access the API with TypeScript.
Listen binds 127.0.0.1 only. Another machine, or a container without host networking, cannot reach it. Harness routes under /__emulator have no auth, so listen refuses any other host.

Prerequisites

  • Node.js 18+
  • An API key string that starts with sk_. The emulator checks that the Authorization: Bearer value matches the key you passed to createApiEmulator. It does not call Soundlink to validate the key.

1. Install

2. Start the emulator

listen() with no port binds an ephemeral port on 127.0.0.1. server.baseUrl is http://127.0.0.1:<port>. Pass that value as baseUrl. The SDK adds /v1. Do not pass a URL that already ends in /v1. Same process, no TCP: emulator.dispatch({ method, path, headers, body }).

3. Create, read metrics, stop

Create debits dailyBudget × durationDays immediately and returns status creating. reserved stays 0. available equals balance.
With plan: "enterprise" (fee 0.1) and a starting balance of 500, that sequence moves the wallet 500 → 430 → 455.56. Overview after set_spend(40) is spend_media 40, fees 4.44, spend_total 44.44. The refund on stop is 25.56. activate and set_spend are harness calls. They are not Public API routes. activate seeds listeners at 120 and does not set spend. Only set_spend raises spend_media. Create returns { campaignId, status: "creating", autoRenew }. generation (3) is on the later GET, not on the 201.

Call it from another process

Fix the port and leave the process running. Any local fetch or curl on the same machine can call it. The client does not need the SDK.
set_spend sends { "action": "set_spend", "mediaSpendUsd": 40 }. Spend must be between 0 and the committed amount, and only while the campaign is creating, active, or inactive, and only before a refund.

Harness

Prefer emulator.transition, emulator.setWallet, emulator.reset, and emulator.failNext when this process owns the emulator. failNext (or POST /__emulator/fault) takes timeout, conflict, or server_error. conflict is one 409 idempotency_key_conflict. server_error is 500 internal_error for the call and the SDK’s default retries, then the next call succeeds. timeout writes nothing on listen. In-process dispatch returns hang: true and status 408, which is not sent on the socket. A second arm while one is pending is rejected. The fault does not change the wallet, and reset clears it. A missing API key still returns 401 and leaves the fault armed. A campaign that never spends is refunded in full on stop. Stopping while status is still creating returns 400. The path for a campaign that never went live is reject_meta, then stop. There is no calendar end date. end is the harness action that settles the cycle. A second stop on stopped or ended does not refund again.

Plans and refunds

Reconcile against GET /v1/wallet. The emulator does not keep a second credit ledger. A refund is the wallet credit on stop or end. Presets are local test values, not the public price list. Pass feeRate when the test needs a specific rate. feeRate overrides the plan. It must be in [0, 1). With a fee, media spend grosses up before the refund:
Omit plan and feeRate and the fee is 0, so the refund is committed − mediaSpend. emulator.getPlan() returns the fee and a premium flag. premium is metadata only. It does not change /v1 payloads.

Insufficient credit

A create that the wallet cannot pay returns 402 with code insufficient_credit. error.details.available is the current balance. error.details.required is the committed debit. Create 402 does not store the Idempotency-Key in this emulator, so the same key would also succeed after a top-up. Do not rely on that. The public API can replay a recorded 402. Retry with a new key.

Options

Errors

The SDK returns { data, error } and does not throw on these responses. See Errors. Stop hashes { campaignId } only. Stopping while creating releases the key, so the same key works after activate.

Not production

Genre, Spotify URL shape, and strategyType are not validated. A value production would reject with 422 can return 201 here.
  • Overview ignores the date query. Invalid date order is not a 400.
  • Implemented routes: GET /v1/ping, GET /v1/wallet, GET /v1/strategies, campaign create, get, list, and stop, GET /v1/campaigns/{id}/metrics/overview, and pixel list, save, delete, and test-events.
  • A pixel test event does not call Meta. A saved pixel returns 200 with status: "sent". Send health stays ok. meta_pixel_conflict and pixel_in_use are not returned.
  • Soundlinks, budget changes, tiers, activity, and JSONL exports are not implemented.
  • campaignUrl on detail is a placeholder. It is not a live dashboard for these ids.

Worked example

soundlink-api-emulator-example seeds six campaigns and serves them at http://127.0.0.1:3000. The emulator stays on http://127.0.0.1:8787.
npm run check asserts the single-campaign wallet 500 → 430 → 455.56, a Meta rejection that does not refund until stop, and the 402 retry with a new Idempotency-Key. The page balance is the shared wallet after every seeded campaign, not the refund of one campaign.

Next

Access the API with TypeScript → Creating campaigns → Errors