Saltar al contenido principal

POST /credentials/issue

Emite credenciales a uno o múltiples recipientes.

  • 1 recipiente: Emisión síncrona. Retorna la credencial inmediatamente.
  • 2+ recipientes: Emisión asíncrona. Retorna un job_id para consultar el progreso.

Request

POST /api/v1/credentials/issue

Headers

HeaderRequeridoDescripción
X-API-KeySiTu API key
Content-TypeSiapplication/json

Body

{
"template_id": "550e8400-e29b-41d4-a716-446655440000",
"recipients": [
{
"email": "[email protected]",
"recipient_name": "Juan Perez",
"result_value": "A",
"credits_earned": 6.0
}
]
}

Campos del recipiente

CampoTipoRequeridoDescripción
emailstringSiempreEmail del recipiente
recipient_namestringSegún templateNombre completo
result_valuestringSegún templateCalificación/resultado
credits_earnednumberSegún templateCréditos obtenidos
evidencearraySegún templateEvidencia del trabajo
rolestringSegún templateRol del recipiente
license_numberstringSegún templateNúmero de licencia
termstringSegún templatePeríodo académico
activity_start_datestringSegún templateFecha inicio (YYYY-MM-DD)
activity_end_datestringSegún templateFecha fin (YYYY-MM-DD)

Estructura de evidence

{
"evidence": [
{
"name": "Proyecto Final",
"url": "https://ejemplo.com/proyecto",
"description": "Implementacion de API REST en Python"
}
]
}

Consulta GET /templates/\{id\} para saber que campos son requeridos por cada template.

Response: Emisión síncrona (1 recipiente)

{
"mode": "sync",
"credentials": [
{
"email": "[email protected]",
"status": "issued",
"credential_id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
"credential_url": "https://app.unicreda.com/credential/7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
}
]
}

Response: Emisión asíncrona (2+ recipientes)

{
"mode": "async",
"job_id": "abc123-task-id",
"message": "Issuing 50 credentials in background. Poll GET /api/v1/jobs/abc123-task-id for status."
}

Usa GET /jobs/\{job_id\} para consultar el progreso.

Detección automática interno/externo

La API detecta automáticamente si el email corresponde a un miembro de tu organización:

  • Interno: Se enlazan automáticamente al wallet del usuario + se auto-llenan campos del perfil
  • Externo: Se crea la credencial y se envia por email con un enlace publico

Errores por recipiente

Si un recipiente tiene datos inválidos en modo sincrono:

{
"mode": "sync",
"credentials": [
{
"email": "[email protected]",
"status": "error",
"errors": ["Calificación inválida. Valores permitidos: A, B, C, D, F"]
}
]
}

Idempotency

Cada job_id actúa como idempotency key implícito del lote.

Si el sistema reintenta procesar el mismo job (worker reinicia, glitch de red, redelivery del broker), las credenciales ya emitidas no se duplican: la base de datos garantiza unicidad por (achievement_id, email_normalizado, job_id). El email se normaliza con la misma lógica que usamos para detectar duplicados dentro de un lote (lowercase + sin dots/aliases para Gmail).

En el response de GET /jobs/\{job_id\} aparece el campo already_emitted con el número de recipients que se detectaron como ya emitidos en una entrega anterior. No se cuenta como error.

{
"job_id": "abc123-task-id",
"status": "SUCCESS",
"result": {
"total": 50,
"successful": 48,
"failed": 0,
"already_emitted": 2,
"errors": [],
"emails_sent": 48,
"emails_failed": 0,
"internal_count": 10,
"external_count": 40
}
}
nota

Llamar POST /credentials/issue dos veces con los mismos recipients pero como dos requests distintos crea credenciales nuevas — cada request genera un job_id distinto. La idempotency es por job, no global. Si necesitás idempotency cross-request, deduplicá del lado del integrador antes de hacer el POST.

Arquitectura interna (informativa)

A partir de la versión actual, la emisión asíncrona puede correr en una de dos rutas internas, controladas server-side por un feature flag:

  • Ruta legacy: el worker procesa todas las filas en un solo task con un commit final.
  • Ruta chord: el worker parte el lote en chunks de 100 filas que corren con su propio commit; un callback final agrega resultados, corre rules de pathway y envía los emails. Si un chunk falla por un transitorio (DB lenta, glitch de red), solo ese chunk se reintenta hasta 3 veces con backoff de 1, 5 y 15 minutos.

Para el integrador no hay diferencia visible: el mode, el job_id, el polling, y el shape del response del result son idénticos. La única diferencia perceptible es la granularidad de progress.current (ver GET /jobs/{id}): en la ruta chord, el contador avanza en saltos de 100 (cuando un chunk termina), en lugar de fila por fila.

El switch entre rutas no rompe la API ni cambia la semántica de idempotency descrita arriba.

Ejemplo: Emisión masiva con Python

import requests
import time

API_KEY = "uc_live_tu_key_aqui"
BASE_URL = "https://app.unicreda.com/api/v1"
headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# Emitir a múltiples recipientes
response = requests.post(f"{BASE_URL}/credentials/issue", headers=headers, json={
"template_id": "550e8400-e29b-41d4-a716-446655440000",
"recipients": [
{"email": f"estudiante{i}@ejemplo.com", "recipient_name": f"Estudiante {i}", "result_value": "A"}
for i in range(1, 51)
]
})

data = response.json()
if data["mode"] == "async":
job_id = data["job_id"]

# Polling del progreso
while True:
status = requests.get(f"{BASE_URL}/jobs/{job_id}", headers=headers).json()
print(f"Status: {status['status']}")

if status["status"] == "SUCCESS":
print(f"Resultado: {status['result']}")
break
elif status["status"] == "FAILURE":
print(f"Error: {status['error']}")
break

time.sleep(2) # Esperar 2 segundos entre consultas