Try it

Menu

Invitations

Put one person on a campaign’s invitation list, read the invitation back, and hear when they join.

Invite one person

POSThttps://campaign.udao.org/api/v1/invitations

Every call carries the header Authorization: Bearer camp_sk_live_..., your API key, and nothing else.

Answers 202 with the invitation as it stands and a reason, for any request the API key may make. An address already on the list is a reason, never an error.

curl https://campaign.udao.org/api/v1/invitations \
  -H "Authorization: Bearer camp_sk_trial_..." \
  -H "Content-Type: application/json" \
  -d '{
      "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
      "email": "ada@example.com",
      "external_id": "your-invitation-id-5e21",
      "send": true
    }'

The answer

202 Accepted
{
  "external_id": "your-invitation-id-5e21",
  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
  "status": "invited",
  "reason": "added",
  "sent": true,
  "blocked": null,
  "received_at": "2026-09-11T09:12:04Z",
  "invitation_url": "https://campaign.udao.org/api/v1/invitations/your-invitation-id-5e21?campaign_id=8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
  "join_url": "https://campaign.udao.org/campaigns/8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74/join?via=partner"
}

An address already on the list under another id of yours answers 202 too, and names that id:

202 Accepted
{
  "external_id": "your-invitation-id-5e21",
  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
  "status": "invited",
  "reason": "already_listed",
  "sent": false,
  "blocked": null,
  "received_at": "2026-09-11T09:14:31Z",
  "invitation_url": "https://campaign.udao.org/api/v1/invitations/your-invitation-id-1c07?campaign_id=8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
  "join_url": "https://campaign.udao.org/campaigns/8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74/join?via=partner",
  "details": {
    "external_id": "your-invitation-id-1c07"
  }
}

What the answer carries

FieldWhat it is
statusinvited, joined or removed: where the invitation stands now.
sentTrue when this call sent the invitation message.
blockedWhy a message you asked for did not go, as a code below, or null.
invitation_urlThe read of this invitation, with campaign_id in place: under details.external_id where the answer carries one, else under your own id.
join_urlThe campaign’s join link with your app’s mark, for a message of your own.
details.external_idOn already_listed only: the id the address is already listed under.

The body

FieldTypeRequiredWhat it is
campaign_idstringRequiredThe campaign whose list the person goes on. A campaign of another organization answers 404. At most 64 characters.
emailstringRequiredThe person’s address, as your dashboard’s Invite people takes it. It is kept on the list until they join or are taken off it, and never sent back. Never a hash: we mail the address when send is true, and the join checks it against the person’s own verified address. At most 254 characters.
external_idstringRequiredYour own id for this invitation, unique on the campaign. Sent again with the same address it answers duplicate; the read and the webhook name it. Never the person’s address, which we drop when they join: an id that is an address, or carries this one, answers 422. At most 200 characters.
sendbooleanRequiredtrue sends the campaign’s invitation message, the one your dashboard sends; false only puts the address on the list. No default: a message cannot be taken back.

Idempotency-Key is accepted as a header and must equal external_id where both are sent. They disagree, 422.

The reasons

ReasonWhat it meansWhat to do
addedThe address is on the list now. sent says whether the message went, and blocked names why not where you asked for it.Nothing. Read the invitation, or wait for invitation.joined.
already_listedThe address was on the list already. If it had no id, it takes yours; if it had another, details.external_id names it. Nothing was sent.Read it at invitation_url, and expect invitation.joined under details.external_id where the answer carries one, else under your own id. Where details.external_id is there, the id you sent names nothing.
duplicateThis id was sent before with this address. The answer is the invitation as it stands, and nothing was written or sent. A retry sent while the first call is still running can be answered before that call settles.Nothing. A new invitation takes a new id. Where it matters, read the invitation: the read is the truth.
removed_from_listThe address was taken off the list, by your organization or by the person from the message. The API never adds it back.Nothing. An owner can add it again in your dashboard.

Messages not sent

An API invitation is your dashboard’s own: the same list, message, limits and unsubscribe as Invite people, and your owners see the row there. The address stays on the list either way. An owner can send the link again from your dashboard.

blockedWhat it means
send_not_availableThe campaign is not live: not open yet (a draft, or waiting for its funding) or past its end. Or Udao has suspended your organization.
domain_proof_requiredA funded campaign sends once your organization has proved its domain.
not_configuredThis deployment could not send mail.
send_refusedThe mail provider refused the message.
send_cap_reachedYour organization has sent the 1,000 invitations it sends in a day.
send_count_unreadableToday’s count of invitations could not be read, so nothing was sent.
recipient_budget_reachedThat mailbox has had as much mail from Campaign as it takes in a short time.
resend_too_soonThat address had a message inside the last day.
resend_exhaustedThat address has had the 3 messages one invitation sends.
invitation_bouncedThat address rejected an earlier message for good.

The errors

Branch on code. message is a sentence for a person and may change. details carries the facts its code names.

StatusCodeWhat it means
401not_authenticatedNo API key, or one that was revoked.
403browser_requestThe request carried a browser Origin header. Call this from your server: an API key in a page is an API key you have published.
403power_not_grantedThe API key tried to send an invitation on a real campaign. An owner allows that in Integrations.
404campaign_not_foundNo campaign of yours has that id. A campaign of another organization answers the same way, and so does one outside this API key’s scope.
409trial_only_credentialAn API key for trial campaigns only named a campaign that is not a trial.
409campaign_not_openThe campaign is settled, closed or canceled, so nobody else can be invited to it.
409invitation_cap_reachedThe list is full. details.scope is campaign for the campaign’s own cap, or organization for the 20 an organization invites before Udao approves it.
413payload_too_largeThe body is past the bound of 100 KB.
415invalid_requestThe body was not sent as application/json.
422invalid_requestA field is missing, unreadable or not one this call takes, and the answer names it. An external_id already naming another address on the campaign is one of them, and so is an external_id that is an address.
429rate_limitedPast the quota: the same 120 writes a minute as the outcomes write. retry_after says when.
500server_errorOur fault. Send the same request again: one external_id never adds twice.
500not_configuredThis deployment lacks a setting it needs to keep a list. Nothing was recorded.

Read an invitation

GEThttps://campaign.udao.org/api/v1/invitations/{external_id}?campaign_id={campaign_id}

The invitation as it stands, by your own id, with the campaign named.

curl -G \
  -H "Authorization: Bearer camp_sk_trial_..." \
  "https://campaign.udao.org/api/v1/invitations/your-invitation-id-5e21" \
  -d campaign_id=8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74
200 OK
{
  "external_id": "your-invitation-id-5e21",
  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
  "status": "joined",
  "invited_at": "2026-09-11T09:12:04Z",
  "sends": 1,
  "last_sent_at": "2026-09-11T09:12:05Z",
  "joined_at": "2026-09-11T13:58:40Z",
  "simulated": true
}

The states

StatusWhat it means
invitedOn the list. Nobody holding the address has taken a place yet.
joinedSomebody holding the invited address took a place, however they joined: from your invitation, the join link or the campaign’s page. joined_at on the read says when.
removedTaken off the list, by your organization or by the person.

The other fields

FieldWhat it is
invited_atWhen the address went on the list.
sendsHow many invitation messages went to the address, at most 3.
last_sent_atWhen the last message went, or null.
joined_atWhen the person took their place, or null before that.
simulatedTrue on a trial campaign.

The read’s errors

StatusCodeWhat it means
401not_authenticatedNo API key, or one that was revoked.
404campaign_not_foundNo campaign of yours has that id. A campaign of another organization answers the same way, and so does one outside this API key’s scope.
404invitation_not_foundNo invitation of that campaign has that id. An invitation added in your dashboard with no id answers the same way, and so does an id another organization sent.
409trial_only_credentialAn API key for trial campaigns only named a campaign that is not a trial.
422invalid_requestcampaign_id is missing. The read names the campaign the invitation belongs to.
429rate_limitedPast the quota: 600 reads a minute per organization, shared with every read your API keys make. retry_after says when.
500server_errorOur fault. Send the same request again: a read changes nothing.
500not_configuredThis deployment lacks a setting it needs to answer. Nothing was recorded.

When they join

The person signs in on Campaign and joins themselves. invitation.joined is sent once somebody holding the address takes a place, if your webhook URL is set and verified. Its body carries your external_id, the campaign_id, status joined, joined_at and simulated. The read is the truth.