---
title: "One contact's history in this sequence"
method: GET
path: "/sequences/{sequence_id}/contacts/{enrolment_id}/history/"
tags: ["sequences"]
---

# One contact's history in this sequence

`GET /sequences/{sequence_id}/contacts/{enrolment_id}/history/`

## Request

- Base URL: `https://api.getoneprofile.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://api.getoneprofile.ai/sequences/{sequence_id}/contacts/{enrolment_id}/history/`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

Everything the sequence did to one contact, oldest first: the emails it sent with their subject and body, the LinkedIn actions it ran with their message, the branch each condition took, and each step it moved them past without doing it, with why (no email address, an email verdict the sequence does not send to, no LinkedIn URL).

Assembled from stores that each remember a different part. The send fence knows an email was accepted and when; the send log knows what it said; the action queue is both at once for LinkedIn; the condition ledger knows which way a branch went, and the skip ledger which steps were passed, because the engine moves the pointer and forgets.

upcoming_skip says whether the step the contact waits on will be skipped once they reach it, read from their row as it is today.

Where a step went through the approval queue, the entry also says what was decided, when, and by whom, as a name. A rejected email appears only this way: it never reached the send fence.

The contact's own part is there too: their reply, with what they wrote and the inbox conversation it opens, and the email a bounce came back for. Beside the timeline, every filled column of their audience row.

## Path parameters

- `sequence_id` string, uuid, required
- `enrolment_id` string, uuid, required

## Response `200`

Successful Response

- ContactHistoryReadSchema: A contact's whole history, oldest first, and what their audience row says about them.
  - `contact_fields` ContactFieldReadSchema[], required: Every filled column of the contact's audience row, in the table's order, as the approval queue shows them. Empty when there is no row to read.
    - `name` string, required
    - `value` string, required
  - `entries` ContactHistoryEntryReadSchema[], required
    - `at` string, date-time, required
    - `body` string, nullable: The email's words as they were sent, without the signature and opt-out footer the send added below them (`email_html` has the whole email), or the text of the contact's reply (trimmed, and cut short past about a thousand characters).
    - `decided_at` string, date-time, nullable
    - `decided_by_display_name` string, nullable: Who decided, as a name. Null when nobody was asked, or when the channel recorded a decision without saying who made it.
    - `decision` 'approved' | 'rejected', nullable: What a person decided about this step from the approval queue, if asked.
    - `detail` string, nullable: The email's subject, the LinkedIn message, or nothing.
    - `email_html` string, nullable: An email's HTML part exactly as it was handed to Gmail: the words, the sender's signature and the opt-out footer, every line, blank line and space as the contact got them. Null for a send from before the send kept its email, and on every entry that is not an email.
    - `entered_at` string, date-time, nullable: When the contact reached this step, as distinct from when it ran. The gap between the two is the step's wait.
    - `kind` 'email' | 'linkedin' | 'condition' | 'reply', required: What one entry in a contact's history is.
    - `note` string, nullable: One plain sentence to show under the outcome where it needs saying, such as which address cannot receive email on a bounce.
    - `outcome` string, required: accepted/failed for a send, or bounced on the email a bounce came back for; the action's own status for LinkedIn; skipped for an email or LinkedIn step the contact was moved past without it being done, with note saying why; yes/no for a condition, and the branch taken (connected, already_connected, not_accepted) for a connection request's answer; replied for the contact's reply.
    - `reason` 'accepted' | 'already_connected' | 'expired' | 'declined' | 'sender_changed' | 'connect_not_offered' | 'email_required' | 'profile_not_resolved' | 'vanity_changed' | 'skipped_in_panel' | 'missed_window' | 'not_sent' | 'no_linkedin_url' | 'own_profile': Why a contact left a connection request by the branch it took. Stored as the condition evaluation's ``reason`` beside the branch, because the branch alone does not say enough: Not accepted covers an invitation that expired, one that was declined, one nobody can answer any more, and a request that could never be sent at all. The values up to ``SENDER_CHANGED`` are answers (``ALREADY_CONNECTED`` the one known before any request went out); every value after it is a reason no request went out, and each one takes Not accepted. ``ACCEPTED`` and ``ALREADY_CONNECTED`` take the Connected branch, the latter its own branch when the request gives already-connected contacts one. ``EXPIRED`` is an invitation still pending when its window closed, ``DECLINED`` one a reading of the profile showed gone without a connection (declined, or withdrawn by hand). ``SENDER_CHANGED`` is one nobody can answer any more: the sender's LinkedIn account was bound to another member, or deleted, after it went out.
    - `spam_signals` string[]: What the spam check said about this message as it went out, one sentence per finding. Empty when it raised nothing. A finding never held the send back; it is here so a reply that never came has an explanation to read.
    - `step_id` string, uuid, nullable
    - `step_name` string, required
    - `step_position` integer, nullable: The step's customer-visible ordinal, zero-based.
    - `thread_id` string, uuid, nullable: The inbox conversation this entry opens, on the reply entry. Null when the contact has no conversation in the inbox.
  - `upcoming_skip` UpcomingSkipReadSchema: The step the contact waits on, which will be skipped when they reach it.
    - `note` string, required: Why, as one plain sentence.
    - `reason` 'no_email' | 'email_verdict' | 'no_linkedin_url' | 'own_profile' | 'profile_not_resolved' | 'nothing_to_withdraw', required: Why the engine passed a step without doing it, for a contact that step cannot reach. Kept on the skip ledger at the moment of the skip, because nothing else remembers it: the engine moves the contact on, and the status cell the skip wrote is rewritten by the next step's line. The first two are an email step's reasons, the rest a LinkedIn step's. A step the browser skipped is not here: its LinkedIn action keeps that ending itself.
    - `step_id` string, uuid, required
    - `verdict` 'valid' | 'catch-all' | 'risky' | 'unknown' | 'invalid' | 'disposable': The verdicts a Verify email column answers with, in the words its cells hold. The ``verify_email`` chain respells every supplier's verdict onto exactly these words (``email_verdict_canonical`` in ``table_enrichments``), so a sequence that reads the column compares its cells against them. Ordered from the safest to send to, which is the order the Audience tab lists them.

## Other responses

- `401`: Unauthorized
- `403`: Forbidden
- `404`: Not found.
- `422`: Invalid request made to the API.
- `500`: Internal server error.
- `502`: Bad gateway error.
- `503`: Service unavailable.

---

[API](https://skmtc.dev/getoneprofile/apis/api-reference-oneprofile.md) · [All operations](https://skmtc.dev/getoneprofile/apis/api-reference-oneprofile/llms.txt) · [OpenAPI document](https://skmtc.dev/getoneprofile/apis/api-reference-oneprofile/revisions/4a6bd71e3f02?raw)
