Skip to content
How-to guide

How to Invite an Actor to a Case

Use this guide to bring an actor into a case it was not present for. An invitation asks rather than asserts, so the actor joins only if it accepts. You finish with the actor either seated as a participant or recorded as having declined.


Prerequisites

  • A running Vultron instance you can drive. The demo containers are the quickest route — see Tutorial: Run the Receive-Report Demo for the two steps that get you a container shell.
  • An actor identity on that instance, holding the role each step names.
  • Working knowledge of the ActivityStreams verb, object, and target shape, summarized in Vultron and ActivityPub.
  • Working knowledge of the case model: the objects a case is made of, in The Case Model, and who is allowed to change them, in The CASE_MANAGER and the Case Ledger. Where a guide's messages are case-scoped, they route through the CASE_MANAGER and their results appear on the case ledger.

  • An existing case, and the Case Owner role on it.

  • The actor's Uniform Resource Identifier (URI).
  • The CASE_MANAGER's actor URI. It sends the invitation and receives the reply.

The exchange

The sequence diagram below shows both outcomes. The Case Owner asks the CASE_MANAGER for the invitation with one Offer. Every later message is between the CASE_MANAGER and the invited actor.

---
title: Invite Actor to Case
---
sequenceDiagram
    actor O as Case Owner
    participant CA as CASE_MANAGER
    actor A as Actor
    activate O
    O ->> CA: Offer(actor=CaseOwner, object=Actor, target=Case, suggestedRoles)
    activate CA
    CA ->>+ A: Invite(actor=CASE_MANAGER, object=Actor, target=CaseStub, attributedTo=CaseOwner)
    note over CA: Records inert CaseParticipant (RM Received)
    note over A: Consider invitation
    alt Accept Invitation
        A -->> CA: Accept(object=Invite)
        note over CA: Participant becomes active (RM stays Received)<br/>then sends case, ledger replay and full-case Invite
    else Reject Invitation
        A -->> CA: Reject(object=Invite)
        note over CA: Closes inert record (RM Closed); kept as history
    end
    deactivate A
    deactivate CA
    deactivate O

Send the invitation

  1. Trigger the invitation as Case Owner. Your container sends your own Offer(Actor) to the CASE_MANAGER, with the invited actor as its object, the case as its target, and the roles you offer in suggestedRoles (CM-17-007). With no roles given, the CASE_MANAGER assigns a non-empty default role list (CM-16-003). A stub Invite always names at least one role, and the CASE_MANAGER refuses to send one with none (CM-11-019).

    Ask the Case Manager to invite an actor

    Asks the Case Manager to invite the actor named by the object to the case named by the target, with the roles in suggestedRoles. For an actor with no record on the case, or an invitee that has not answered, the Case Manager answers with the Stub Invite to that actor, not with an offer to the Case Owner.

    • When… the Case Owner decides to invite an actor to the case
    • Send: Offer(Actor)[target=VulnerabilityCase]
    • Send to: the case manager
    • Sent by: a participant
    • Distinguished by: sender is the case owner
    • Reference: Ask the Case Manager to invite an actor
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Offer",
      "id": "urn:uuid:c561571e-1b26-478a-9401-f40980f62122",
      "name": "https://vultron.example/organizations/vendorco Offer Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
      "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
      "to": "https://vultron.example/case-actors/vendorco-case-actor",
      "actor": "https://vultron.example/organizations/vendorco",
      "target": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
      "object": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Organization",
        "id": "https://vultron.example/organizations/coordinator",
        "name": "Coordinator LLC",
        "published": "2026-10-23T20:17:06+00:00",
        "updated": "2026-10-23T20:17:06+00:00",
        "inbox": {
          "@context": "https://www.w3.org/ns/activitystreams",
          "type": "OrderedCollection",
          "id": "https://vultron.example/organizations/coordinator/inbox",
          "items": []
        },
        "outbox": {
          "@context": "https://www.w3.org/ns/activitystreams",
          "type": "OrderedCollection",
          "id": "https://vultron.example/organizations/coordinator/outbox",
          "items": []
        }
      },
      "suggestedRoles": [
        "coordinator"
      ]
    }
    
  2. The CASE_MANAGER commits and sends Invite(Actor) to the actor's inbox, with a case stub as its target rather than the case itself (CM-11-013, CM-17-001), with itself as the ActivityStreams actor and your identity in attributedTo (PCR-08-007, PCR-08-008). The trigger's 202 response means only that your Offer was queued, so watch for the Invite on the CASE_MANAGER's ledger, not in the response.

    Invite actor to case

    Sends a Stub Invite carrying the case stub and the embargo terms the invitee would agree to.

    • When… the Case Manager invites an actor to join the case
    • Send: Invite(Actor)[target=VulnerabilityCaseStub]
    • Send to: a participant
    • Sent by: the case manager
    • Reference: Invite actor to case
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Invite",
      "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/invitation/1",
      "name": "https://vultron.example/organizations/vendorco Invite Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub",
      "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
      "content": "We're inviting you to participate in VendorCo Case #70930046.",
      "to": "https://vultron.example/organizations/coordinator",
      "actor": "https://vultron.example/organizations/vendorco",
      "target": {
        "@context": "https://certcc.github.io/Vultron/ns/context.jsonld",
        "type": "VulnerabilityCaseStub",
        "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub",
        "summary": "A memory-corruption vulnerability in widget — details shared after acceptance.",
        "caseId": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0"
      },
      "object": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Organization",
        "id": "https://vultron.example/organizations/coordinator",
        "name": "Coordinator LLC",
        "published": "2026-10-23T20:17:06+00:00",
        "updated": "2026-10-23T20:17:06+00:00",
        "inbox": {
          "@context": "https://www.w3.org/ns/activitystreams",
          "type": "OrderedCollection",
          "id": "https://vultron.example/organizations/coordinator/inbox",
          "items": []
        },
        "outbox": {
          "@context": "https://www.w3.org/ns/activitystreams",
          "type": "OrderedCollection",
          "id": "https://vultron.example/organizations/coordinator/outbox",
          "items": []
        }
      }
    }
    
  3. Set the reply deadline on the activity's end_time. When it is present that value settles precedence over the invitee's local policy window; when it is absent the policy window applies instead (CM-28-002, ADR-0065). Either way the effective deadline is clamped down to the embargo's own end_time, so an end_time that outlives the embargo does not buy the invitee extra time (EP-07-006).

The CASE_MANAGER is the sender, not the Case Owner

Putting the Case Owner in the actor field makes the invitation unrecognizable to a conformant peer, which expects case-management handshakes to come from the CASE_MANAGER. Record who asked for the invitation in attributedTo instead.


Answer an invitation

Send your reply to the CASE_MANAGER, never to the Case Owner.

  • If you are joining the case, send Accept(Invite(Actor)) with the Invite activity as its object.

    Accept invite to case

    Accepts the Stub Invite. The actor joins the case at RM Received.

    • When… an invited actor agrees to join the case
    • Send: Accept(Invite(Actor)[target=VulnerabilityCaseStub])
    • Send to: the case manager
    • Sent by: the invitee
    • Reference: Accept invite to case
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Accept",
      "id": "urn:uuid:9495c01d-0739-472e-affe-b1625ad9775c",
      "name": "https://vultron.example/organizations/coordinator Accept https://vultron.example/organizations/vendorco Invite Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub",
      "inReplyTo": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/invitation/1",
      "content": "We're accepting your invitation to participate in VendorCo Case #70930046.",
      "to": "https://vultron.example/organizations/vendorco",
      "actor": "https://vultron.example/organizations/coordinator",
      "object": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Invite",
        "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/invitation/1",
        "name": "https://vultron.example/organizations/vendorco Invite Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub",
        "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
        "published": "2026-06-01T19:12:00+00:00",
        "updated": "2026-06-01T19:12:00+00:00",
        "content": "We're inviting you to participate in VendorCo Case #70930046.",
        "to": "https://vultron.example/organizations/coordinator",
        "actor": "https://vultron.example/organizations/vendorco",
        "target": {
          "@context": "https://certcc.github.io/Vultron/ns/context.jsonld",
          "type": "VulnerabilityCaseStub",
          "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub",
          "summary": "A memory-corruption vulnerability in widget — details shared after acceptance.",
          "caseId": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0"
        },
        "object": {
          "@context": "https://www.w3.org/ns/activitystreams",
          "type": "Organization",
          "id": "https://vultron.example/organizations/coordinator",
          "name": "Coordinator LLC",
          "published": "2026-10-23T20:17:06+00:00",
          "updated": "2026-10-23T20:17:06+00:00",
          "inbox": {
            "@context": "https://www.w3.org/ns/activitystreams",
            "type": "OrderedCollection",
            "id": "https://vultron.example/organizations/coordinator/inbox",
            "items": []
          },
          "outbox": {
            "@context": "https://www.w3.org/ns/activitystreams",
            "type": "OrderedCollection",
            "id": "https://vultron.example/organizations/coordinator/outbox",
            "items": []
          }
        }
      }
    }
    
  • If you are not joining, send Reject(Invite(Actor)) with the Invite as its object.

    Reject invite to case

    Rejects the Stub Invite, which closes the invitee's participant record.

    • When… an invited actor declines to join the case
    • Send: Reject(Invite(Actor)[target=VulnerabilityCaseStub])
    • Send to: the case manager
    • Sent by: the invitee
    • Reference: Reject invite to case
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Reject",
      "id": "urn:uuid:4ed0846e-fb26-4e78-95aa-145dad4a1e5d",
      "name": "https://vultron.example/organizations/coordinator Reject https://vultron.example/organizations/vendorco Invite Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub",
      "inReplyTo": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/invitation/1",
      "content": "Thanks for the invitation, but we're declining to participate in VendorCo Case #70930046.",
      "to": "https://vultron.example/organizations/vendorco",
      "actor": "https://vultron.example/organizations/coordinator",
      "object": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Invite",
        "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/invitation/1",
        "name": "https://vultron.example/organizations/vendorco Invite Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub",
        "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
        "published": "2026-06-01T19:12:00+00:00",
        "updated": "2026-06-01T19:12:00+00:00",
        "content": "We're inviting you to participate in VendorCo Case #70930046.",
        "to": "https://vultron.example/organizations/coordinator",
        "actor": "https://vultron.example/organizations/vendorco",
        "target": {
          "@context": "https://certcc.github.io/Vultron/ns/context.jsonld",
          "type": "VulnerabilityCaseStub",
          "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub",
          "summary": "A memory-corruption vulnerability in widget — details shared after acceptance.",
          "caseId": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0"
        },
        "object": {
          "@context": "https://www.w3.org/ns/activitystreams",
          "type": "Organization",
          "id": "https://vultron.example/organizations/coordinator",
          "name": "Coordinator LLC",
          "published": "2026-10-23T20:17:06+00:00",
          "updated": "2026-10-23T20:17:06+00:00",
          "inbox": {
            "@context": "https://www.w3.org/ns/activitystreams",
            "type": "OrderedCollection",
            "id": "https://vultron.example/organizations/coordinator/inbox",
            "items": []
          },
          "outbox": {
            "@context": "https://www.w3.org/ns/activitystreams",
            "type": "OrderedCollection",
            "id": "https://vultron.example/organizations/coordinator/outbox",
            "items": []
          }
        }
      }
    }
    

On acceptance, the CASE_MANAGER commits the reply to the ledger, seats you at Report Management (RM) state RM.RECEIVED, signs your embargo consent if an embargo is active, sends Announce(VulnerabilityCase) to seed your replica, and backfills the earlier ledger entries (CM-17-004).

Announce vulnerability case

Sends the full case to the newly admitted Participant once its acceptance clears the embargo consent checks.

  • When… the Case Manager has admitted a new Participant to the case
  • Send: Announce(VulnerabilityCase)
  • Send to: a participant
  • Sent by: the case manager
  • Reference: Announce vulnerability case
Example activity
{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Announce",
  "id": "urn:uuid:e73f24db-c3b4-4cae-b880-920d28d04730",
  "name": "https://vultron.example/organizations/vendorco Announce VendorCo Case #70930046",
  "actor": "https://vultron.example/organizations/vendorco",
  "object": {
    "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
    "type": "VulnerabilityCase",
    "name": "VendorCo Case #70930046",
    "published": "2026-10-23T20:17:07+00:00",
    "updated": "2026-10-23T20:17:07+00:00",
    "attributedTo": "https://vultron.example/organizations/vendorco",
    "caseParticipants": [
      {
        "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/finndervul",
        "type": "CaseParticipant",
        "name": "Finn der Vul",
        "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
        "published": "2026-10-23T20:17:07+00:00",
        "updated": "2026-10-23T20:17:07+00:00",
        "attributedTo": "https://vultron.example/users/finndervul",
        "caseRoles": [
          "reporter"
        ],
        "participantStatuses": [
          {
            "id": "urn:uuid:92d8b666-d2e4-4431-9ae4-07826f5627e9",
            "type": "ParticipantStatus",
            "name": "START",
            "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
            "published": "2026-10-23T20:17:07+00:00",
            "updated": "2026-10-23T20:17:07+00:00",
            "attributedTo": "https://vultron.example/users/finndervul",
            "rmState": "START",
            "caseEngagement": true,
            "cvdRole": [
              "REPORTER"
            ],
            "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
          }
        ],
        "embargoConsents": [],
        "joined": true,
        "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
      },
      {
        "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/vendorco",
        "type": "CaseParticipant",
        "name": "VendorCo",
        "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
        "published": "2026-10-23T20:17:07+00:00",
        "updated": "2026-10-23T20:17:07+00:00",
        "attributedTo": "https://vultron.example/organizations/vendorco",
        "caseRoles": [
          "vendor"
        ],
        "participantStatuses": [
          {
            "id": "urn:uuid:910bbee1-16d1-41a6-bdff-0591c85e10a5",
            "type": "ParticipantStatus",
            "name": "RECEIVED Vf",
            "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
            "published": "2026-10-23T20:17:07+00:00",
            "updated": "2026-10-23T20:17:07+00:00",
            "attributedTo": "https://vultron.example/organizations/vendorco",
            "rmState": "RECEIVED",
            "vfState": "Vf",
            "caseEngagement": true,
            "cvdRole": [
              "OBSERVER"
            ],
            "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
          }
        ],
        "embargoConsents": [],
        "joined": true,
        "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
      }
    ],
    "actorParticipantIndex": {
      "https://vultron.example/users/finndervul": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/finndervul",
      "https://vultron.example/organizations/vendorco": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/vendorco"
    },
    "vulnerabilityReports": [
      {
        "id": "https://demo.vultron.local/VulnerabilityReport/e28f4965-7838-4644-b4a8-10604c7ee92f",
        "type": "VulnerabilityReport",
        "name": "FDR-8675309",
        "published": "2026-10-23T20:17:06+00:00",
        "updated": "2026-10-23T20:17:06+00:00",
        "content": "I found a vulnerability!",
        "attributedTo": "https://vultron.example/users/finndervul",
        "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
      }
    ],
    "caseStatuses": [
      {
        "id": "urn:uuid:f6eb7e92-a7c7-4a65-962b-2d3d2c936d25",
        "type": "CaseStatus",
        "name": "NONE pxa",
        "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
        "published": "2026-10-23T20:17:07+00:00",
        "updated": "2026-10-23T20:17:07+00:00",
        "attributedTo": "https://vultron.example/organizations/vendorco",
        "emState": "NONE",
        "pxaState": "pxa",
        "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
      }
    ],
    "notes": [],
    "embargoRegister": [],
    "pendingEmbargoProposalIndex": {},
    "caseActivity": [],
    "genesisHash": "61f38fb955f8b894dab64e9149c18c5c95eb77848ed54ea9fb554d912281add1",
    "ledgerPosition": {
      "logIndex": -1,
      "entryHash": "61f38fb955f8b894dab64e9149c18c5c95eb77848ed54ea9fb554d912281add1"
    },
    "parentCases": [],
    "childCases": [],
    "siblingCases": [],
    "activeParticipants": [
      "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/finndervul",
      "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/vendorco"
    ],
    "@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
  }
}

Expect RM.RECEIVED, not RM.ACCEPTED. Accepting an invitation says you are willing to join the case; it does not say you have validated the report, which you have not yet seen in full (CM-11-001). After the case and the ledger replay reach you, the CASE_MANAGER sends a second Invite, the full-case Invite, and you judge the case by answering it — see How to Manage a Case Roster. Do not answer the original report Offer: you were never offered that report (CM-11-020). Rule on the case afterwards — see How to Advance a Case Through Report Management.

A reply that arrives after the Invite's deadline is still processed. The one exception is an Accept of a stub Invite that a newer one replaced, which the CASE_MANAGER refuses and answers by naming the replacement; a Reject of it is honored (CM-11-014, CM-11-016). A replacement or re-invite names the Invite it replaces in inReplyTo (CM-11-015, CM-11-016). A Reject or an Accept from an actor with no participant record is refused (CM-11-018, CM-11-021). An Accept of a stub Invite from an actor that already joined the case, or was removed from it, is a duplicate: the CASE_MANAGER records and sends nothing (CM-17-012).


Choose invitation over seating

Use as:Invite for an actor that was absent when the case was created. Seat the Case Owner and any already-known participants, such as the Reporter, inline on the as:Create for the case instead — see How to Initialize a Case.

If you are not the Case Owner but you know an actor belongs on the case, suggest it rather than inviting it: see How to Suggest an Actor for a Case.


Verify

What you sent What to confirm
Invite(Actor) The invitee holds an Invite whose actor is the CASE_MANAGER.
Accept(Invite(Actor)) The case roster holds you, and you have a local case replica.
Reject(Invite(Actor)) The CASE_MANAGER's ledger records your Reject; your participant entry is closed (RM Closed) and kept as history, but you are not an active participant.

See it end to end

Try it: vultron-demo invite-actor

vultron-demo invite-actor

Or with Docker Compose:

DEMO=invite-actor docker compose -f docker/docker-compose.yml run --rm demo

The scenario invites one coordinator that accepts and a second that rejects.


Further reading