Every lead is sold once. One buyer, no resale, no shared pools. How exclusivity works
HTTP post Webhooks REST

Delivery API reference

Written from the buyer side. What we post to your endpoint, every field it carries, what we expect back, how retries and signatures work, and the REST endpoints for pulling reports and pushing outcomes back.

How delivery works

Six conventions hold across everything on this page.

HTTPS and JSON

One POST per lead, over TLS, with a UTF-8 JSON body and a content type of application/json. Timestamps are RFC 3339 in UTC and phone numbers are E.164, both directions.

Credentials from onboarding

A signing secret, an API token, and optionally the bearer token you want us to present to your endpoint. There is no self-serve key page, because a delivery carries consumer contact data and a consent record.

The lead id is the key

Idempotency on delivery is the lead_id. It is also the key for credits, consent requests, dispositions, and support. Store it on your record before anything else.

Every delivery is signed

An HMAC over the timestamp header and the raw body, keyed with your signing secret. Verify against the raw bytes, compare in constant time, and reject stale timestamps.

At-least-once delivery

Failures and timeouts are retried on a backoff schedule with the same lead id and a higher attempt number. Build the consumer to be idempotent rather than assuming exactly-once.

One error envelope

Errors from the REST API carry a machine-readable type and code alongside the message and a request id. See the error format.

This page documents the shape of the contract. The host you call, the authoritative schemas, the full enumerated value sets, and your credentials are issued during onboarding.

Credentials come from onboarding

There is no self-serve key generator, and that is deliberate. Every payload on this page carries a real person, their phone number, and the record of what they agreed to.

  • A signing secret. Used to verify that a delivery came from us and has not been replayed. Rotate it on request without interrupting delivery.
  • An API token. Bearer auth for the buyer-facing endpoints, scoped to your account and restrictable to read-only.
  • Your endpoint credential. If your endpoint expects a bearer token or a shared secret header, give it to us and we present exactly that on every post.
  • Test traffic first. Sample leads travel the same path with test set to true, so the mapping is proven before a live record moves. See test leads.
An agent working delivered leads on a monitor in a dark office

The request we make to your endpoint

A single POST, sent within seconds of the form submission. This is the whole delivery mechanism.

POST /webhooks/solved-marketing HTTP/1.1
Host: crm.example-agency.com
Content-Type: application/json
User-Agent: SolvedMarketing-Delivery/1
X-Solvedmarketing-Lead-Id: lead_01K5R7TQ3M9B
X-Solvedmarketing-Delivery: dlv_01K5R7TQ4F2C
X-Solvedmarketing-Attempt: 1
X-Solvedmarketing-Timestamp: 1789056000
X-Solvedmarketing-Signature: v1=6b41f0c8d2a7e93b5c
Authorization: Bearer BUYER_TOKEN_FROM_ONBOARDING

{
  "lead_id": "lead_01K5R7TQ3M9B",
  "program_id": "prog_fex_core",
  "vertical": "final_expense",
  "test": false,
  "created_at": "2026-09-18T14:19:46Z",
  "delivered_at": "2026-09-18T14:19:52Z",
  "contact": {
    "first_name": "Marjorie",
    "last_name": "Alvarado",
    "phone": "+14045550143",
    "phone_type": "mobile",
    "city": "Atlanta",
    "state": "GA",
    "postal_code": "30309",
    "county": "Fulton",
    "date_of_birth": "1957-04-02",
    "age": 69,
    "time_zone": "America/New_York"
  },
  "final_expense": {
    "coverage_reason": "burial_and_final_costs",
    "monthly_budget": "50_to_75",
    "existing_coverage": false,
    "tobacco": false,
    "health_flags": ["none_reported"],
    "beneficiary_intent": "adult_children"
  },
  "consent": {
    "captured_at": "2026-09-18T14:19:44Z",
    "ip_address": "198.51.100.37",
    "page_url": "https://offer.example/fex/quote",
    "disclosure_id": "disc_fex_2026_03",
    "form_duration_seconds": 74
  },
  "source": {
    "channel": "paid_social",
    "campaign_ref": "fex-ga-q3",
    "device": "mobile"
  }
}

# what we want back
HTTP/1.1 200 OK
Content-Type: application/json

{
  "received": true,
  "lead_id": "lead_01K5R7TQ3M9B",
  "your_record_id": "CRM-884213"
}

# a duplicate you already hold
HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "received": true,
  "duplicate": true,
  "lead_id": "lead_01K5R7TQ3M9B"
}

# a rejection we should not retry
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "received": false,
  "reason": "state_not_licensed",
  "lead_id": "lead_01K5R7TQ3M9B"
}

A response body is optional and we do not require any particular shape. If you send one, your_record_id is stored against the lead so a later support conversation can be held in your identifiers rather than ours.

Request headers

Eight headers, four of which exist so you can deduplicate and verify without parsing the body.

HeaderWhat it carries
Content-TypeAlways application/json. The body is UTF-8 encoded JSON.
User-AgentIdentifies the delivery client and its major version, for example SolvedMarketing-Delivery/1.
AuthorizationA bearer token you supply at onboarding, if your endpoint requires one. We send exactly what you give us.
X-Solvedmarketing-Lead-IdThe lead id, duplicated out of the body so you can deduplicate without parsing.
X-Solvedmarketing-DeliveryA unique identifier for this delivery attempt. Changes on every retry.
X-Solvedmarketing-AttemptThe attempt number, starting at 1. Anything above 1 is a retry of a delivery you may already hold.
X-Solvedmarketing-TimestampUnix seconds at signing time. Part of the signed payload; reject deliveries outside your tolerance window.
X-Solvedmarketing-SignatureVersion marker and HMAC over the timestamp and the raw body. See verifying a signature.

Lead fields

Seven groups. The envelope, contact, consent, and source blocks are identical in every vertical; only the vertical block changes.

Envelope

Present on every lead, in every vertical. These are the fields your integration should key on.

FieldTypeDescription
lead_id string Stable identifier for the lead. The key for credits, consent requests, dispositions, and support. Store it.
program_id string The program the lead came from. One program is one vertical, one filter set, one delivery path, and one pacing configuration.
vertical enum One of medicare, life, or final_expense. Tells you which vertical block to read.
test boolean True for onboarding and regression traffic. Test leads are never billed and never count against a cap.
created_at string RFC 3339 UTC timestamp of the form submission.
delivered_at string RFC 3339 UTC timestamp of this delivery attempt. Subtract your first dial time from this to get speed to dial.

Contact and identity

Identical across verticals. Nested under contact.

FieldTypeDescription
first_name string Given name as the prospect entered it.
last_name string Family name as the prospect entered it.
phone string E.164, for example +14045550143. Validated before delivery.
phone_type enum mobile, landline, or voip, from the validation run before delivery.
email string Optional. Omitted when the form did not collect it or the prospect declined.
address_line1 string Street address. Optional on programs that do not collect it.
city string City name.
state string Two-letter USPS code.
postal_code string Five-digit ZIP.
county string County name without the word county. Present because licensing and plan availability are frequently county-level.
date_of_birth string ISO 8601 date. Omitted on programs that collect an age band instead.
age integer Derived from date of birth at submission time, so your fit rules do not have to compute it.
gender enum Optional. Present only where the prospect supplied it.
time_zone string IANA zone derived from the address. Dial on this rather than on your office time zone.

Medicare block

Nested under medicare, present when vertical is medicare.

FieldTypeDescription
plan_interest enum advantage, part_d, or supplement, captured at the form rather than guessed at on the call.
currently_enrolled boolean Whether the prospect says they already hold a Medicare plan.
enrollment_period enum aep, ma_oep, sep, or age_in. The period that makes this prospect eligible to move.
turning_65 boolean True for age-in demand.
eligibility_date string ISO 8601 date, present on age-in and special enrollment records where the prospect gave one.
medicaid_flag boolean Optional. Present only on programs that ask.

Life block

Nested under life, present when vertical is life.

FieldTypeDescription
product_interest enum term or permanent. Separate programs, because they are separate prospects.
coverage_amount enum The face amount band the prospect asked about, so the agent opens with the right product.
term_length enum Present when the prospect named one, for example 20_year.
tobacco boolean Self-reported tobacco use.
beneficiary_intent enum Why they are shopping. The difference between a quote and a conversation.
health_flags array Self-reported condition flags, enough at the form to route to an agent who can place the case.

Final expense block

Nested under final_expense, present when vertical is final_expense.

FieldTypeDescription
coverage_reason enum What the coverage is for, usually burial and end-of-life costs.
monthly_budget enum The monthly premium band the prospect had in mind, so the agent builds to budget rather than guessing.
existing_coverage boolean Whether the prospect already holds a policy.
tobacco boolean Self-reported tobacco use.
health_flags array Self-reported condition flags that predict whether a simplified issue product will actually place.
beneficiary_intent enum Who the prospect is trying to protect.

Nested under consent, identical across verticals. Captured at the form, because it cannot be reconstructed afterward.

FieldTypeDescription
captured_at string RFC 3339 UTC timestamp of the agreement itself.
ip_address string The address the submission came from.
page_url string The full URL of the page the prospect was on.
disclosure_id string Identifier for the exact, versioned disclosure block shown. This is the field to store.
disclosure_text string The language the prospect actually saw, not a template.
user_agent string The browser user agent string at submission.
form_duration_seconds integer How long the prospect spent on the form.

Source and attribution

Nested under source. Deliberately coarse: enough to tune your own reporting, not the media buying detail.

FieldTypeDescription
channel enum The broad acquisition channel, for example paid_social or search.
campaign_ref string A stable reference for the campaign concept. Tell us when one outperforms on your floor.
device enum mobile, desktop, or tablet.
landing_page_ref string A stable reference for the landing page variant.

Fields are added over time and are not removed without notice, so parse defensively and ignore keys you do not recognize. Absent optional fields are omitted rather than sent as null.

Expected responses

What each status code from your endpoint causes on our side.

Your responseWhat happens
200 OKYou accepted the lead. Nothing further happens.
201 CreatedYou accepted the lead and created a record. Treated exactly like a 200.
202 AcceptedYou queued the lead for processing. Treated as accepted; we do not wait for the downstream result.
409 ConflictYou already hold this lead_id. Recorded as delivered and not retried. The correct answer to a duplicate retry.
4xx (other)Treated as a failed delivery and retried. A persistent 4xx usually means credentials or a body your endpoint rejects, and we will contact you.
5xxTreated as a failed delivery and retried on the backoff schedule.
TimeoutNo response inside the response window is a failed delivery. Answer fast and do the work out of band.

You are not billed for a lead we could not deliver. A program failing to deliver is something we raise with you rather than letting it quietly stop.

Retries and idempotency

Delivery is at least once, which is a design constraint rather than an apology. Build for it and duplicates stop being a category of bug.

  • The lead id is the idempotency key. Every retry carries the same lead_id. Key on it, and a retry becomes a no-op instead of a second record.
  • Attempts are numbered. X-Solvedmarketing-Attempt starts at 1 and increments. Anything above 1 is a delivery you may already hold.
  • Backoff, not a hammer. Retries are spaced on an increasing schedule so an endpoint recovering from an incident is not knocked back over.
  • 409 stops the retries. Answering 409 for a lead id you already hold records the delivery as complete rather than continuing the schedule.
  • Undeliverable leads are held. A lead that fails the full schedule is not discarded and not billed. See failed deliveries.
  • Writes you make are idempotent too. Send Idempotency-Key on any POST to the REST API and a repeat returns the original response instead of creating a second resource.

Build the consumer this way

Verify
Check the signature against the raw bytes before you parse anything.
Dedupe
Look up the lead id. If you have it, answer 409 and stop.
Accept
Write the record, return a success status immediately.
Then work
Enrichment, scoring, routing, and notifications all happen after the response.

Verifying a signature

Four steps. The two that catch people are verifying against the raw bytes and rejecting stale timestamps.

  • Read the raw body first. Capture the bytes exactly as received, before any JSON parsing. Reordering a key changes the bytes and breaks the comparison.
  • Build the signed payload. The timestamp header, a period, then the raw body.
  • Compare in constant time. A naive string comparison leaks timing information about the secret.
  • Reject stale timestamps. Anything outside your tolerance window is a replay, even if the signature checks out.

# PHP
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_SOLVEDMARKETING_TIMESTAMP'];
$sig = $_SERVER['HTTP_X_SOLVEDMARKETING_SIGNATURE'];

if (abs(time() - (int) $ts) > 300) {
    http_response_code(400);
    exit;  // outside the tolerance window
}

$expected = 'v1=' . hash_hmac(
    'sha256',
    $ts . '.' . $raw,
    SOLVEDMARKETING_SIGNING_SECRET
);

if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}

$lead = json_decode($raw, true);  // now it is safe

Webhooks and events

Register an HTTPS endpoint, subscribe it to the types you care about, and return a 2xx quickly. Deliveries are signed and retried the same way lead deliveries are.

Event typeWhen it fires
lead.deliveredA lead was posted to your endpoint. The data object is the full lead record.
lead.acceptedYour endpoint answered with a success status. Carries the attempt count and the status code you returned.
lead.rejectedDelivery failed after the full retry schedule, or your endpoint rejected the lead outright. The lead is held and not billed.
lead.creditedA credit was applied to a lead. Carries the category and the decision, so your billing reconciliation can be automatic.
disposition.receivedWe recorded an outcome you sent for a lead. Useful as an acknowledgement that the feedback loop is actually closed.

The event envelope

Identical across event types. Only the data object changes shape.

{
  "id": "evt_01K5R7TV81QD",
  "type": "lead.credited",
  "created_at": "2026-09-19T16:04:11Z",
  "program_id": "prog_fex_core",
  "data": {
    "lead_id": "lead_01K5R7TQ3M9B",
    "credit_id": "crd_01K5R7TV6W0M",
    "category": "unreachable_phone",
    "decision": "approved",
    "submitted_at": "2026-09-19T09:52:03Z",
    "decided_at": "2026-09-19T16:04:08Z"
  }
}

Events are delivered at least once and are not ordered. Key on the event id, or on the lead id for lead events, and reconcile on the timestamps inside the payload rather than on arrival order. The signature scheme is the one described under verifying a signature.

The buyer-facing REST API

Bearer auth over HTTPS, JSON in and out, cursor pagination, idempotent writes. This is how you pull your own numbers and push outcomes back.

Reading leads, deliveries, and reports

Collections return data, has_more, and next_cursor. Cursors are opaque; do not construct them or assume they encode an offset.

EndpointWhat it does
GET /v1/leadsList delivered leads by date range, program, vertical, or delivery state. Cursor paginated.
GET /v1/leads/{lead_id}Fetch one lead exactly as it was delivered, including the consent record.
GET /v1/deliveriesDelivery attempts with their timestamps, attempt counts, and the status your endpoint returned.
GET /v1/programsThe programs on your account with their current caps, delivery windows, and live pacing state.
GET /v1/reports/deliveryDelivered, accepted, rejected, and credited counts by program and day.
GET /v1/reports/outcomesContact, appointment, and issued counts by program and day, computed from the dispositions you have sent.
POST /v1/exportsSchedule a recurring export of leads and delivery records to an endpoint you name. Idempotent.

Posting dispositions and credits back

Outcomes are what turn a lead program from a guess into something tunable on your traffic specifically. Send them natively from AgentTech Dialer or Solved Enroll, over these endpoints, or as a scheduled file if that is genuinely all your stack can do.

EndpointWhat it does
POST /v1/dispositionsRecord an outcome against a lead id: the disposition, the timestamp, and the attempt count. Idempotent.
POST /v1/dispositions/batchThe same thing for up to a day of outcomes in one request, for buyers exporting on a schedule.
POST /v1/creditsSubmit a credit request: lead id, category, and one sentence of detail. Idempotent.
GET /v1/creditsList credit requests with their state and decision.
POST /v1/programs/{id}/pausePause delivery on a program. No penalty, and resumable the same way.
POST /v1/programs/{id}/resumeResume a paused program.

curl -X POST https://api.solvedmarket.ing/v1/dispositions \
  -H "Authorization: Bearer $SOLVEDMARKETING_TOKEN" \
  -H "Idempotency-Key: 4f2b7e10-5c33-4a71-9e08-1c6d" \
  -H "Content-Type: application/json" \
  -d '{"lead_id":"lead_01K5R7TQ3M9B",
      "disposition":"appointment_set",
      "attempts":2,
      "first_attempt_at":"2026-09-18T14:20:31Z",
      "occurred_at":"2026-09-18T14:41:02Z"}'

The disposition set we work from is small on purpose: contacted, not contacted, wrong number, not interested, appointment set, application started, application submitted, policy issued, and do not call. If your system already has its own set, send yours and we map it once. See the disposition set.

Rate limiting and errors

One ceiling, one error envelope, and status codes that mean what they usually mean.

Rate limiting

  • The standard ceiling is 600 requests per minute per account on the buyer-facing endpoints.
  • A 429 carries Retry-After in seconds. Back off on it rather than retrying immediately.
  • Bulk reads are paginated rather than throttled harder. Pull by cursor, or schedule an export instead.
  • Deliveries we make to you do not count against it. Those are paced by your caps and delivery windows.
  • Higher limits are available on request. Describe the traffic shape, not just the peak number.

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "error": {
    "type": "invalid_request",
    "code": "credit_window_closed",
    "message": "That lead is outside the credit
      submission window for this program.",
    "param": "lead_id",
    "request_id": "req_01K5R7TW2K8P"
  }
}

StatusMeaning
400 Bad RequestThe body could not be parsed, or a required parameter is missing or malformed.
401 UnauthorizedNo bearer token, an expired token, or a token that belongs to a different account.
403 ForbiddenAuthenticated but not permitted: a scope the token does not carry, or a program on another account.
404 Not FoundNo such lead, program, or credit, or it is outside the scope of this token.
409 ConflictThe resource is not in a state that allows the operation, such as pausing a program that is already paused.
422 Unprocessable EntityWell formed and understood, but rejected by a rule: a disposition against a lead you were never delivered, or a credit outside the submission window.
429 Too Many RequestsRate limited. Retry-After carries the number of seconds to wait.
5xxOur fault. Safe to retry with the same idempotency key; a retried write will not duplicate.

Quote the request_id when you report a problem. It is the fastest way for us to find the request in our logs.

FAQs

Delivery API questions

Where do credentials come from?

Onboarding, not a self-serve key page. You receive a signing secret for verifying deliveries, an API token for the buyer-facing REST endpoints, and, if your endpoint requires one, we store the bearer token you want us to present when we post to you. All three are scoped to your account and revocable on their own.

Do I have to use the REST API at all?

No. Plenty of buyers only ever receive posts at an endpoint and never call us. The REST API is for pulling delivery and outcome reports into your own reporting, posting dispositions back, and submitting credits programmatically. Buyers on the AgentTech Dialer or Solved Enroll paths get most of that without any code.

How is idempotency handled on delivery?

The lead id is the idempotency key. Every retry of a delivery carries the same lead id with an incrementing attempt header, so a consumer that keys on the lead id will never create two records. If you already hold the lead id, answering 409 tells us so and stops the retries.

What is the response window on a delivery?

Short enough that it should never be a design question: accept the body, return a success status, and do enrichment, scoring, or third-party lookups out of band. An endpoint that answers only after finishing all of its downstream work will be retried while it is still working, and will process the same lead twice.

Is this a specification I can generate a client from?

This page documents the shape of the contract: the request we make, the fields it carries, the responses we act on, the events, and the buyer-facing endpoints. The authoritative schemas, the full set of enumerated values for each field, and the host you call come with your credentials at onboarding.

What are the rate limits?

The standard ceiling on the buyer-facing REST API is 600 requests per minute per account, and a 429 carries a Retry-After header. Bulk reads are paginated rather than throttled harder, so pull by cursor or schedule an export instead. Higher limits are available on request; describe the traffic shape rather than just the peak number.

Do deliveries to my endpoint count against the rate limit?

No. The rate limit applies to requests you make to us. Deliveries we make to you are paced by your program caps and delivery windows, which are the controls that actually decide how much traffic your endpoint sees.

Something else? Contact us

Ready to build against it?

Onboarding issues the signing secret, the API token, and test leads through your endpoint in the same conversation.