External UID

External UID

Prefer metadata for new integrations. Every write endpoint accepts metadata: Record<string, string> — a structured key/value bag echoed on the response and on every webhook event tied to the resource. metadata carries richer reconciliation data (multiple keys, per-key meaning) and is the recommended way to link API resources to your internal ledger. external_uid still works as a single-value shortcut. See Webhooks for how both fields surface on event payloads.

Every write endpoint accepts an optional external_uid field in the request body. The value is persisted on the underlying resource and included on every webhook event payload. The field exists so you can tag an API resource with an identifier from your own system — a campaign ID, an order number, a row PK — and link the two ledgers later without maintaining a separate mapping table.

The field

POST /v2/airdrops
Content-Type: application/json
Authorization: Bearer $PIRATE_API_KEY
Idempotency-Key: 5f3e2c8a-1d1d-4f9b-a2c2-9a1b2c3d4e5f

{
  "payer": "WALLET_PUBKEY",
  "platform_id": "pirate-crew",
  "mint": "MINT_ADDRESS",
  "airdrop_count": 0,
  "merkle_root": "0xabc...",
  "mode": "unsigned",
  "external_uid": "campaign_2026_q2_airdrop_42"
}

The response echoes the value verbatim on the wrapped envelope:

{
  "data": {
    "mode": "unsigned",
    "transaction": "AQABAo...",
    "external_uid": "campaign_2026_q2_airdrop_42"
  },
  "meta": { "request_id": "req_01HZ..." }
}

Constraints:

RuleValue
Length1–128 chars
Character setletters, digits, _, -, ., :
Uniquenessnot enforced — pass the same value to many endpoints if it makes sense
Storagepersisted on the resource; surfaced on webhook deliveries

How it differs from Idempotency-Key

These two fields look similar but solve different problems. Use both.

Idempotency-Keyexternal_uid
Header or bodyHeaderBody
PurposeMake retries safeLink the API resource to your ledger
Server uses it toCache + return the prior response on retryPersist on the resource; echo on webhooks
ScopeSingle logical HTTP operationLong-lived business object (a campaign, an order, a pool launch)
LifetimeTTL'd, cleared after the operation settlesAs long as you want — your value, your meaning
UniquenessMust be fresh per operationReuse freely; the API doesn't enforce uniqueness

The intuition: Idempotency-Key is per HTTP call, external_uid is per business object.

external_uid cross-call linking pattern

A worked example — running a quarterly airdrop campaign:

const externalUid = "campaign_2026_q2_airdrop_42";          // your ledger's campaign row PK

await fetch("/v2/merkle-trees", {
  headers: { "Idempotency-Key": randomUUID() },
  body: JSON.stringify({ ..., external_uid: externalUid }),
});

await fetch("/v2/airdrops", {
  headers: { "Idempotency-Key": randomUUID() },             // new key — different operation
  body: JSON.stringify({ ..., external_uid: externalUid }), // same uid — same campaign
});

// Months later, a webhook arrives:
// { type: "airdrop.claimed", data: { external_uid: "campaign_2026_q2_airdrop_42", wallet, amount } }
// → look up the campaign by external_uid, credit the user in your DB.

Same external_uid across three separate API calls and an inbound webhook, all tied to one row in your campaigns table. Different Idempotency-Key on every call because each is a different operation.

When to use it

Use it on every write you'd later want to look up by your ID rather than the API's:

  • Airdrop campaigns — your campaign row ↔ API tree/init/claim events.
  • Fee splits — your invoice or partner-payout row ↔ on-chain split tx.
  • Token launches — your project record ↔ pool address.
  • NFT mints in a drop — your drop's row ↔ each mint instruction.
  • Pool creates from your launchpad — your launchpad job ↔ on-chain pool.

Read endpoints (GET) don't accept it — there's no resource to tag.

Reconciliation pattern

A reconciliation job in your service typically looks like:

// Daily: pull every webhook delivery for the last 24h, group by external_uid,
// reconcile against your DB.
for (const event of webhooks.deliveriesSince(yesterday)) {
  const row = await db.findCampaignByExternalUid(event.data.external_uid);
  if (!row) continue;                       // not one of ours, skip
  await db.recordEvent(row.id, event.type, event.data);
}

This works even if the API rotates internal IDs, even if you replay deliveries, even if events arrive out of order — because the linking key is something you control rather than something the API generates.

Edge cases

  • Omitted on a retry — the cached response from Idempotency-Key returns whatever the original call sent. If the original call included external_uid and the retry omits it, the response still has it (cached). Don't rely on this for safety — pass external_uid consistently across retries.
  • Different external_uid on a retry with the same Idempotency-Key — the API treats it as a 409 conflict (body hash mismatch). See Idempotency.
  • Sensitive data — don't put PII or secrets here; it will appear in API responses, webhook payloads, and (eventually) the developer-portal request log.

Field reference per endpoint

Currently supported on every write endpoint:

EndpointGroup
POST /v2/pools/{address}/fee-claimsFees
POST /v2/merkle-treesAirdrops
POST /v2/airdropsAirdrops
POST /v2/airdrops/{id}/claimsAirdrops
POST /v2/airdrops/{id}/vault-withdrawalsAirdrops
POST /v2/stakes · DELETE /v2/stakes/{asset}Staking
POST /v2/nfts · DELETE /v2/nfts/{asset}NFTs
POST /v2/gold-locks · DELETE /v2/gold-locks/{user}Gold
POST /v2/tokens · DELETE /v2/tokens/{mint}/authorities/{type}Tokens
POST /v2/transactionsTx

Not accepted on GET endpoints or on POST /v2/transaction-simulations (simulation has no resource to tag).