Try it

Menu

Outcomes

Report an outcome from your server, and read it back.

Report an outcome

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

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

Answers 202 for any well-formed request the API key may make. A fact about the data comes back as a reason, never as an error.

curl https://campaign.udao.org/api/v1/outcomes \
  -H "Authorization: Bearer camp_sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
      "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
      "email": "ada@example.com",
      "reference": "course-solidity-101",
      "occurred_at": "2026-09-11T14:03:22Z",
      "external_id": "your-own-id-8f3a",
      "kind": "completed"
    }'

The answer

202 Accepted
{
  "external_id": "your-own-id-8f3a",
  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
  "status": "pending",
  "reason": "awaiting_review",
  "received_at": "2026-09-11T14:03:25Z",
  "outcome_url": "https://campaign.udao.org/api/v1/outcomes/your-own-id-8f3a?campaign_id=8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74"
}

A write the campaign will not take as evidence answers 202 as well, with the reason and the fact behind it:

202 Accepted
{
  "external_id": "your-own-id-4c71",
  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
  "status": "recorded",
  "reason": "outside_window",
  "received_at": "2026-09-11T13:12:06Z",
  "outcome_url": "https://campaign.udao.org/api/v1/outcomes/your-own-id-4c71?campaign_id=8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
  "details": {
    "grace_ends_at": "2026-09-08T23:59:59Z"
  }
}

The body

FieldTypeRequiredWhat it is
campaign_idstringRequiredThe campaign the outcome belongs to. A campaign of another organization answers 404. At most 64 characters.
emailstringRequired, or email_hashAny address the person has verified on Campaign. Hashed on arrival and never stored.
email_hashstringRequired, or emailThe same address, folded, as HMAC-SHA256 under the Email salt shown beside your API key, hex encoded. Send email or email_hash, never both. At most 64 characters.
referencestringRequiredWhat they did, in your own words. The same word the file you import uses. At most 200 characters.
occurred_atstringRequiredRFC 3339 with a zone. The only input the window rules read. At most 64 characters.
external_idstringRequiredYour own id for this outcome. The dedupe key: a second send of it stores nothing and answers the first reply again. At most 200 characters.
kindstringOptionalcompleted, the default, or revoked to revoke one. A revocation names the external_id of the outcome it revokes and answers 202 with a reason.

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

The reasons

ReasonStatusWhat it meansWhat to do
awaiting_reviewpendingRecorded and waiting for a verifier.Poll the read, or wait for the outcome.verified webhook.
participant_not_joinedrecordedNobody holding a place has verified that address.Send them the join URL in details. This id is spent: sent again it answers the first reply again. It is matched once someone who has verified that address holds a place, inside the campaign’s window.
completed_before_joinrecordedThe outcome is dated before the place was taken, so the campaign did not cause it.Nothing. It stays a record either way. Report the next one.
outside_windowrecordedThe campaign stopped accepting evidence.Read grace_ends_at in details. The outcome is recorded rather than refused, because a late outcome can still be attested by hand.
duplicatethe status the first write answeredThat external_id was written before. The event is stored once, and this is the first answer again, whatever has changed since.Nothing. A second act takes a second id.
decided_by_reviewerthe place’s own stateA person already decided the outcome for this place.Nothing. A write never overrides a person.
reference_not_countedrecordedThis campaign names the references it counts and reference was not one of them. The write is kept as a record and no evidence is written.Check the reference against the ones the campaign was set up with. A campaign that names none counts every reference.
auto_verifiedverifiedThe campaign has auto-verify on, so the outcome verified on its own. Whoever turned it on is named on the record.Nothing. The read answers verified, and the reward is committed to that place under the campaign’s own caps.
withdrew_pendingrejectedkind revoked, on an outcome nobody had decided yet. It will not be verified.Nothing. Report the next outcome under a new external_id.
revoked_for_causerevokedkind revoked, from an API key allowed to revoke, on a verified outcome. It is verified no longer.Nothing. The read answers revoked from here on.
payment_standsrevokedkind revoked, from an API key allowed to revoke, on a paid outcome. Nothing is clawed back and the certificate takes its revoked state.Nothing. The webhook and the read both say paid and payment_stands, so your own product can say the same.
unknown_outcomerecordedkind revoked, naming an external_id this campaign never received.Check the id. It is recorded either way, and a landed revocation gets this same answer.

Your own dashboard prints the same write on its Integrations page, with a badge for its verdict. The badges read recorded as Not counted, pending as Pending, verified as Verified, revoked as Revoked, refused as Refused, failed as Failed. A request we answered 4xx and a verifier’s refusal are both refused, and the response code that says which is behind the row’s Details.

The errors

Every error comes back in one body. This is the endpoint’s answer to an occurred_at with no zone:

422 Unprocessable Content
{
  "error": {
    "code": "invalid_request",
    "message": "occurred_at is an RFC 3339 instant carrying its zone, such as 2026-09-11T14:03:22Z",
    "details": {
      "field": "occurred_at"
    }
  }
}

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.
403power_not_grantedThe API key tried to revoke a verified or paid outcome 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.
404outcome_not_foundThe read: nothing was reported to that campaign under that id.
409campaign_not_importableThat campaign is not at a point where evidence is accepted.
409trial_only_credentialAn API key for trial campaigns only named a campaign that is not a trial. Change the API key, not the campaign.
413payload_too_largeThe body is past the bound. A write runs to at most 100 KB, and details.max_bytes carries the figure.
415invalid_requestThe body was not sent as application/json. The endpoint reads one content type and names it.
422invalid_requestA field is missing, unreadable or not one this endpoint takes. The answer names it.
429rate_limitedPast the quota. retry_after says when, in seconds.
500server_errorOur fault. Send the same request again: one external_id never counts twice.

Read an outcome

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

The state, the reward in human units beside base units, and the certificate where the holder’s passport shows it.

curl -G \
  -H "Authorization: Bearer camp_sk_live_..." \
  "https://campaign.udao.org/api/v1/outcomes/your-own-id-8f3a" \
  -d campaign_id=8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74
200 OK
{
  "external_id": "your-own-id-8f3a",
  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
  "status": "verified",
  "verified_at": "2026-09-11T14:06:02Z",
  "reward": {
    "base_units": "25000000000000000000",
    "amount": "25.00",
    "token_symbol": "UDAO",
    "decimals": 18,
    "simulated": false
  },
  "paid_at": "2026-09-11T14:31:40Z",
  "certificate": {
    "reference": "3b7e9d24-6c1a-4f85-b2d0-9e4a7c1f5b38",
    "url": "https://campaign.udao.org/p/ada/3b7e9d24-6c1a-4f85-b2d0-9e4a7c1f5b38"
  }
}
StatusWhat it means
recordedThe outcome is kept and nothing has been decided about it.
pendingEvidence is written and a verifier has it.
verifiedA verifier approved it. The reward is committed to that place.
rejectedThe outcome does not count. A verifier decided so.
revokedA verified outcome was revoked.
FieldWhat it is
statusThe place’s own state, not the record this report made.
reward.simulatedTrue on a trial campaign. Read it before you print a figure.
certificate.referenceThe certificate’s id.
certificate.urlNull with a reason where the holder keeps their passport unlisted.

Rate limits

WhatThe bound
Writes120 a minute, per organization
Reads600 a minute, per organization
Per IP addressPast 600 requests a minute from one IP address, 429 before your API key is read
Past the quota429 with retry_after, in seconds, also in the Retry-After header